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

CritasWang 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 0acff497175 Fix import-data and export-data correctness and error 
reporting (#18828)
0acff497175 is described below

commit 0acff4971750046b6973c654fe16d7f5c3e9a333
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 22 12:31:33 2026 +0800

    Fix import-data and export-data correctness and error reporting (#18828)
    
    * Fix import-data and export-data correctness and error reporting
    
    * Declare relational grammar dependency for CLI export
    
    * Fix Windows relative source archival test
    
    * Fix archive collisions and typed CSV column names
    
    * Handle import failures per file and share safe TsFile archival
    
    * Fix standalone TsFile import status and parenthesized CSV columns
    
    * Fix ambiguous table CSV imports and custom query validation
---
 iotdb-client/cli/pom.xml                           |    5 +
 .../en/org/apache/iotdb/cli/i18n/CliMessages.java  |   16 +
 .../zh/org/apache/iotdb/cli/i18n/CliMessages.java  |   16 +
 .../org/apache/iotdb/tool/common/Constants.java    |    5 +-
 .../iotdb/tool/common/ImportTsFileOperation.java   |   59 ++
 .../apache/iotdb/tool/data/AbstractExportData.java |   29 +-
 .../apache/iotdb/tool/data/AbstractImportData.java |  197 ++--
 .../org/apache/iotdb/tool/data/ExportData.java     |   12 +-
 .../apache/iotdb/tool/data/ExportDataTable.java    |  164 ++-
 .../org/apache/iotdb/tool/data/ExportDataTree.java |  131 +--
 .../org/apache/iotdb/tool/data/ImportData.java     |    3 +-
 .../apache/iotdb/tool/data/ImportDataTable.java    |   86 +-
 .../org/apache/iotdb/tool/data/ImportDataTree.java |   88 +-
 .../org/apache/iotdb/tool/tsfile/ImportTsFile.java |    9 +-
 .../apache/iotdb/tool/tsfile/ImportTsFileBase.java |  126 +--
 .../iotdb/tool/tsfile/ImportTsFileScanTool.java    |    8 +
 .../iotdb/tool/data/DataToolRegressionTest.java    | 1042 ++++++++++++++++++++
 .../apache/iotdb/tool/tsfile/ImportTsFileTest.java |  205 ++++
 scripts/tools/windows/export-data.bat              |    4 +-
 scripts/tools/windows/export-tsfile.bat            |    4 +-
 scripts/tools/windows/import-data.bat              |    4 +-
 scripts/tools/windows/schema/export-schema.bat     |    4 +-
 scripts/tools/windows/schema/import-schema.bat     |    4 +-
 23 files changed, 1796 insertions(+), 425 deletions(-)

diff --git a/iotdb-client/cli/pom.xml b/iotdb-client/cli/pom.xml
index 5de247a084a..a1e00508740 100644
--- a/iotdb-client/cli/pom.xml
+++ b/iotdb-client/cli/pom.xml
@@ -54,6 +54,11 @@
             <artifactId>iotdb-antlr</artifactId>
             <version>2.0.11-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.iotdb</groupId>
+            <artifactId>iotdb-relational-grammar</artifactId>
+            <version>2.0.11-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.iotdb</groupId>
             <artifactId>node-commons</artifactId>
diff --git 
a/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java 
b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
index 0b0f30d970d..ec00a7dc559 100644
--- 
a/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
+++ 
b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
@@ -76,7 +76,23 @@ public final class CliMessages {
       
MESSAGE_INVALID_ARGS_REQUIRED_VALUES_FOR_OPTION_TABLE_NOT_PROVIDED_4BC3FCFA =
           "Invalid args: Required values for option table not provided.";
 
+  // Data import/export
+  public static final String EXCEPTION_FAILED_TO_INSERT_CSV_BATCH_ARG_159E0269 
=
+      "Failed to insert CSV batch: %s";
+  public static final String 
MESSAGE_ARG_UNEXPECTED_ERROR_OCCURRED_ARG_E7255EE6 =
+      "[%s] - Unexpected error occurred: %s";
+  public static final String
+      
EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768
 =
+          "SQL/TsFile export requires a query from a single table in database 
%s.";
+  public static final String
+      
MESSAGE_QUERY_TO_EXPORT_TABLE_SQL_TSFILE_QUERIES_REQUIRE_A_SINGLE_TABLE_WITHOUT_JOINS_SUBQUERIES_OR_CTES_OVERRIDES_TABLE_START_TIME_AND_END_TIME_OPTIONAL_CC12D400
 =
+          "Query to export. Table SQL/TsFile queries require a single table 
without joins, subqueries or CTEs. Overrides -table, -start_time and -end_time. 
(optional)";
+  public static final String
+      MESSAGE_MULTIPLE_CSV_HEADERS_RESOLVE_TO_THE_SAME_TARGET_COLUMN_99189563 =
+          "Multiple CSV headers resolve to the same target column.";
+
   private CliMessages() {}
+
   // 
---------------------------------------------------------------------------
   // Additional auto-collected messages
   // 
---------------------------------------------------------------------------
diff --git 
a/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java 
b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
index a260732ab55..8321a28d08e 100644
--- 
a/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
+++ 
b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
@@ -76,7 +76,23 @@ public final class CliMessages {
       
MESSAGE_INVALID_ARGS_REQUIRED_VALUES_FOR_OPTION_TABLE_NOT_PROVIDED_4BC3FCFA =
           "参数无效:未提供 table 选项的必填值。";
 
+  // Data import/export
+  public static final String EXCEPTION_FAILED_TO_INSERT_CSV_BATCH_ARG_159E0269 
=
+      "CSV 批次写入失败:%s";
+  public static final String 
MESSAGE_ARG_UNEXPECTED_ERROR_OCCURRED_ARG_E7255EE6 =
+      "[%s] - 发生意外错误:%s";
+  public static final String
+      
EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768
 =
+          "SQL/TsFile 导出要求查询仅来自数据库 %s 中的一张表。";
+  public static final String
+      
MESSAGE_QUERY_TO_EXPORT_TABLE_SQL_TSFILE_QUERIES_REQUIRE_A_SINGLE_TABLE_WITHOUT_JOINS_SUBQUERIES_OR_CTES_OVERRIDES_TABLE_START_TIME_AND_END_TIME_OPTIONAL_CC12D400
 =
+          "导出查询。表模型 SQL/TsFile 查询要求仅包含一张表,不支持联接、子查询或 CTE。指定后忽略 
-table、-start_time 和 -end_time。(可选)";
+  public static final String
+      MESSAGE_MULTIPLE_CSV_HEADERS_RESOLVE_TO_THE_SAME_TARGET_COLUMN_99189563 =
+          "多个 CSV 表头映射到同一目标列。";
+
   private CliMessages() {}
+
   // 
---------------------------------------------------------------------------
   // Additional auto-collected messages
   // 
---------------------------------------------------------------------------
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
index ed2d96dc860..b6999dea427 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
@@ -19,6 +19,8 @@
 
 package org.apache.iotdb.tool.common;
 
+import org.apache.iotdb.cli.i18n.CliMessages;
+
 import org.apache.tsfile.enums.TSDataType;
 
 import java.text.SimpleDateFormat;
@@ -222,7 +224,8 @@ public class Constants {
   public static final String QUERY_COMMAND_NAME = "query";
   public static final String QUERY_COMMAND_ARGS_NAME = "query_command";
   public static final String QUERY_COMMAND_DESC =
-      "The query command that you want to execute.If sql_dialect is table The 
'q' parameter is only applicable to export types of CSV, and is not available 
for other types.If the '- q' parameter is not empty, then the parameters' 
creatTime ',' EndTime 'and' table 'are not effective.(optional)";
+      CliMessages
+          
.MESSAGE_QUERY_TO_EXPORT_TABLE_SQL_TSFILE_QUERIES_REQUIRE_A_SINGLE_TABLE_WITHOUT_JOINS_SUBQUERIES_OR_CTES_OVERRIDES_TABLE_START_TIME_AND_END_TIME_OPTIONAL_CC12D400;
 
   public static final String TARGET_FILE_ARGS = "pfn";
   public static final String TARGET_FILE_NAME = "prefix_file_name";
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/ImportTsFileOperation.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/ImportTsFileOperation.java
index 3769d855d16..9934efbef6f 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/ImportTsFileOperation.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/ImportTsFileOperation.java
@@ -21,6 +21,15 @@ package org.apache.iotdb.tool.common;
 
 import org.apache.iotdb.cli.utils.IoTPrinter;
 
+import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
 public enum ImportTsFileOperation {
   NONE,
   MV,
@@ -29,6 +38,56 @@ public enum ImportTsFileOperation {
   DELETE,
   ;
 
+  /** Process a TsFile together with its sidecars without replacing existing 
archive files. */
+  public void processFileGroup(Path source, Path target) throws IOException {
+    List<String> suffixes = new ArrayList<>();
+    suffixes.add("");
+    for (String suffix : new String[] {".resource", ".mods"}) {
+      if (Files.exists(Paths.get(source + suffix))) {
+        suffixes.add(suffix);
+      }
+    }
+    if (this == CP || this == HARDLINK || this == MV) {
+      // Check every destination before moving any file in the TsFile/sidecar 
group.
+      for (String suffix : suffixes) {
+        Path destination = Paths.get(target + suffix);
+        if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
+          throw new FileAlreadyExistsException(destination.toString());
+        }
+      }
+      Files.createDirectories(target.getParent());
+    }
+    for (String suffix : suffixes) {
+      ImportTsFileOperation operation = this == HARDLINK && !suffix.isEmpty() 
? CP : this;
+      operation.processFile(Paths.get(source + suffix), Paths.get(target + 
suffix));
+    }
+  }
+
+  private void processFile(Path source, Path target) throws IOException {
+    switch (this) {
+      case DELETE:
+        Files.deleteIfExists(source);
+        break;
+      case CP:
+        Files.copy(source, target);
+        break;
+      case HARDLINK:
+        try {
+          Files.createLink(target, source);
+        } catch (FileAlreadyExistsException e) {
+          throw e;
+        } catch (IOException | UnsupportedOperationException e) {
+          Files.copy(source, target);
+        }
+        break;
+      case MV:
+        Files.move(source, target);
+        break;
+      default:
+        break;
+    }
+  }
+
   public static boolean isValidOperation(String operation) {
     return "none".equalsIgnoreCase(operation)
         || "mv".equalsIgnoreCase(operation)
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
index f1d506a72b4..e3c33950d42 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.tool.data;
 
 import org.apache.iotdb.cli.utils.IoTPrinter;
+import org.apache.iotdb.isession.SessionDataSet;
 import org.apache.iotdb.rpc.IoTDBConnectionException;
 import org.apache.iotdb.rpc.RpcUtils;
 import org.apache.iotdb.rpc.StatementExecutionException;
@@ -30,6 +31,8 @@ import org.apache.thrift.TException;
 import java.time.Instant;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -40,7 +43,31 @@ public abstract class AbstractExportData extends 
AbstractDataTool {
   public abstract void init()
       throws IoTDBConnectionException, StatementExecutionException, TException;
 
-  public abstract void exportBySql(String sql, int index);
+  public abstract boolean exportBySql(String sql, int index);
+
+  protected static String formatSqlValue(String value, String type) {
+    if (value == null) {
+      return "NULL";
+    }
+    if ("TEXT".equalsIgnoreCase(type) || "STRING".equalsIgnoreCase(type)) {
+      return "'" + value.replace("'", "''") + "'";
+    }
+    return value;
+  }
+
+  protected static List<String> getCsvHeaders(SessionDataSet dataSet, boolean 
treeModel) {
+    List<String> headers = new ArrayList<>(dataSet.getColumnNames());
+    if (Boolean.TRUE.equals(needDataTypePrinted)) {
+      List<String> types = dataSet.getColumnTypes();
+      for (int i = 0; i < headers.size(); i++) {
+        String name = headers.get(i);
+        if (!"Time".equalsIgnoreCase(name) && !(treeModel && 
"Device".equals(name))) {
+          headers.set(i, String.format("%s(%s)", name, types.get(i)));
+        }
+      }
+    }
+    return headers;
+  }
 
   protected static void legalCheck(String sql) {
     String aggregatePattern =
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractImportData.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractImportData.java
index fa80c8feb20..8aef206c61b 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractImportData.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractImportData.java
@@ -19,25 +19,54 @@
 
 package org.apache.iotdb.tool.data;
 
+import org.apache.iotdb.cli.i18n.CliMessages;
 import org.apache.iotdb.cli.utils.IoTPrinter;
 import org.apache.iotdb.rpc.IoTDBConnectionException;
 import org.apache.iotdb.rpc.StatementExecutionException;
 import org.apache.iotdb.tool.common.Constants;
+import org.apache.iotdb.tool.common.ImportTsFileOperation;
 import org.apache.iotdb.tool.tsfile.ImportTsFileScanTool;
 
 import java.io.File;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public abstract class AbstractImportData extends AbstractDataTool implements 
Runnable {
 
   private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
+  private static final AtomicBoolean importFailed = new AtomicBoolean();
+
+  @FunctionalInterface
+  protected interface DataInsertion {
+    void insert() throws IoTDBConnectionException, StatementExecutionException;
+  }
+
+  /** Retry connection failures without discarding the caller's batch when 
insertion fails. */
+  protected static void executeWithRetry(DataInsertion insertion, int 
retryTime) {
+    for (int attempt = 0; ; attempt++) {
+      try {
+        insertion.insert();
+        return;
+      } catch (IoTDBConnectionException e) {
+        if (attempt >= retryTime) {
+          throw new IllegalStateException(
+              String.format(
+                  
CliMessages.EXCEPTION_FAILED_TO_INSERT_CSV_BATCH_ARG_159E0269, e.getMessage()),
+              e);
+        }
+      } catch (StatementExecutionException e) {
+        throw new IllegalStateException(
+            String.format(
+                CliMessages.EXCEPTION_FAILED_TO_INSERT_CSV_BATCH_ARG_159E0269, 
e.getMessage()),
+            e);
+      }
+    }
+  }
 
   public abstract void init()
       throws InterruptedException, IoTDBConnectionException, 
StatementExecutionException;
@@ -45,35 +74,55 @@ public abstract class AbstractImportData extends 
AbstractDataTool implements Run
   @Override
   public void run() {
     String filePath = "";
-    File file = null;
-    try {
-      if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(fileType)) {
-        while ((filePath = ImportTsFileScanTool.pollFromQueue()) != null) {
-          file = new File(filePath);
+    if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(fileType)) {
+      while ((filePath = ImportTsFileScanTool.pollFromQueue()) != null) {
+        File file = new File(filePath);
+        try {
           if (file.getName().endsWith(Constants.TSFILE_SUFFIXS)) {
             importFromTsFile(file);
           }
+        } catch (Exception e) {
+          handleUnexpectedError(file, e);
         }
-      } else {
-        while ((filePath = ImportDataScanTool.pollFromQueue()) != null) {
-          file = new File(filePath);
+      }
+    } else {
+      while ((filePath = ImportDataScanTool.pollFromQueue()) != null) {
+        File file = new File(filePath);
+        try {
           if (file.getName().endsWith(Constants.SQL_SUFFIXS)) {
             importFromSqlFile(file);
           } else {
             importFromCsvFile(file);
           }
+        } catch (Exception e) {
+          handleUnexpectedError(file, e);
         }
       }
-    } catch (Exception e) {
-      ioTPrinter.println(
-          String.format("[%s] - Unexpected error occurred: %s", 
file.getName(), e.getMessage()));
     }
   }
 
+  private static void handleUnexpectedError(File file, Exception e) {
+    markFileFailed();
+    ioTPrinter.println(
+        String.format(
+            CliMessages.MESSAGE_ARG_UNEXPECTED_ERROR_OCCURRED_ARG_E7255EE6,
+            file.getName(),
+            e.getMessage()));
+  }
+
+  protected static void markImportFailed() {
+    importFailed.set(true);
+  }
+
+  protected static void markFileFailed() {
+    markImportFailed();
+    loadFileFailedNum.increment();
+  }
+
   protected abstract Runnable getAsyncImportRunnable();
 
   protected class ThreadManager {
-    public void asyncImportDataFiles() {
+    public boolean asyncImportDataFiles() {
       List<Thread> list = new ArrayList<>(threadNum);
       for (int i = 0; i < threadNum; i++) {
         Thread thread = new Thread(getAsyncImportRunnable());
@@ -85,16 +134,21 @@ public abstract class AbstractImportData extends 
AbstractDataTool implements Run
             try {
               thread.join();
             } catch (InterruptedException e) {
+              importFailed.set(true);
               Thread.currentThread().interrupt();
               ioTPrinter.println("ImportData thread join interrupted: " + 
e.getMessage());
             }
           });
-      ioTPrinter.println(Constants.IMPORT_COMPLETELY);
+      if (!importFailed.get()) {
+        ioTPrinter.println(Constants.IMPORT_COMPLETELY);
+      }
+      return !importFailed.get();
     }
   }
 
-  public static void init(AbstractImportData instance) {
-    instance.new ThreadManager().asyncImportDataFiles();
+  public static boolean init(AbstractImportData instance) {
+    importFailed.set(false);
+    return instance.new ThreadManager().asyncImportDataFiles();
   }
 
   protected abstract void importFromSqlFile(File file);
@@ -111,6 +165,7 @@ public abstract class AbstractImportData extends 
AbstractDataTool implements Run
         processingLoadSuccessfulFileSuccessfulNum.increment();
         ioTPrinter.println("Processed success file [ " + file + " ] 
successfully!");
       } catch (final Exception processSuccessException) {
+        importFailed.set(true);
         ioTPrinter.println(
             "Failed to process success file [ "
                 + file
@@ -120,106 +175,11 @@ public abstract class AbstractImportData extends 
AbstractDataTool implements Run
     }
   }
 
-  protected void processingFile(final String file, final boolean isSuccess) {
-    final String relativePath = 
file.substring(ImportTsFileScanTool.getSourceFullPathLength() + 1);
-    final Path sourcePath = Paths.get(file);
-
-    final String target =
-        isSuccess
-            ? successDir
-            : failDir + File.separator + relativePath.replace(File.separator, 
"_");
-    final Path targetPath = Paths.get(target);
-
-    final String RESOURCE = ".resource";
-    Path sourceResourcePath = Paths.get(sourcePath + RESOURCE);
-    sourceResourcePath = Files.exists(sourceResourcePath) ? sourceResourcePath 
: null;
-    final Path targetResourcePath = Paths.get(target + RESOURCE);
-
-    final String MODS = ".mods";
-    Path sourceModsPath = Paths.get(sourcePath + MODS);
-    sourceModsPath = Files.exists(sourceModsPath) ? sourceModsPath : null;
-    final Path targetModsPath = Paths.get(target + MODS);
-
-    switch (isSuccess ? successOperation : failOperation) {
-      case DELETE:
-        {
-          try {
-            Files.deleteIfExists(sourcePath);
-            if (null != sourceResourcePath) {
-              Files.deleteIfExists(sourceResourcePath);
-            }
-            if (null != sourceModsPath) {
-              Files.deleteIfExists(sourceModsPath);
-            }
-          } catch (final Exception e) {
-            ioTPrinter.println(String.format("Failed to delete file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case CP:
-        {
-          try {
-            Files.copy(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            if (null != sourceResourcePath) {
-              Files.copy(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.copy(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            ioTPrinter.println(String.format("Failed to copy file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case HARDLINK:
-        {
-          try {
-            Files.createLink(targetPath, sourcePath);
-          } catch (FileAlreadyExistsException e) {
-            ioTPrinter.println("Hardlink already exists: " + e.getMessage());
-          } catch (final Exception e) {
-            try {
-              Files.copy(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            } catch (final Exception copyException) {
-              ioTPrinter.println(
-                  String.format("Failed to copy file: %s", 
copyException.getMessage()));
-            }
-          }
-
-          try {
-            if (null != sourceResourcePath) {
-              Files.copy(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.copy(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            ioTPrinter.println(
-                String.format("Failed to copy resource or mods file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case MV:
-        {
-          try {
-            Files.move(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            if (null != sourceResourcePath) {
-              Files.move(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.move(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            ioTPrinter.println(String.format("Failed to move file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      default:
-        break;
-    }
+  protected void processingFile(final String file, final boolean isSuccess) 
throws IOException {
+    final String relativePath = ImportTsFileScanTool.getRelativePath(file);
+    final Path target = Paths.get(isSuccess ? successDir : 
failDir).resolve(relativePath);
+    final ImportTsFileOperation operation = isSuccess ? successOperation : 
failOperation;
+    operation.processFileGroup(Paths.get(file), target);
   }
 
   protected void processFailFile(final String filePath, final Exception e) {
@@ -232,6 +192,7 @@ public abstract class AbstractImportData extends 
AbstractDataTool implements Run
       }
 
       loadFileFailedNum.increment();
+      importFailed.set(true);
       ioTPrinter.println("Failed to import [ " + filePath + " ] file: " + 
e.getMessage());
 
       try {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
index 322f10e059b..63a2fdf45c7 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
@@ -236,15 +236,21 @@ public class ExportData extends AbstractDataTool {
           ioTPrinter.println(sql);
           String[] values = sql.trim().split(";");
           for (int i = 0; i < values.length; i++) {
-            exportData.exportBySql(values[i], i);
+            if (!exportData.exportBySql(values[i], i)) {
+              exitCode = Constants.CODE_ERROR;
+            }
           }
         } else {
-          exportData.exportBySql(null, 0);
+          if (!exportData.exportBySql(null, 0)) {
+            exitCode = Constants.CODE_ERROR;
+          }
         }
       } else {
         String[] values = queryCommand.trim().split(";");
         for (int i = 0; i < values.length; i++) {
-          exportData.exportBySql(values[i], i);
+          if (!exportData.exportBySql(values[i], i)) {
+            exitCode = Constants.CODE_ERROR;
+          }
         }
       }
     } catch (IOException e) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
index 1694e86c9f0..d60b5d559b6 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
@@ -19,7 +19,12 @@
 
 package org.apache.iotdb.tool.data;
 
+import org.apache.iotdb.cli.i18n.CliMessages;
 import org.apache.iotdb.cli.utils.IoTPrinter;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.parser.CaseInsensitiveStream;
+import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlBaseVisitor;
+import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlLexer;
+import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlParser;
 import org.apache.iotdb.isession.ITableSession;
 import org.apache.iotdb.isession.SessionDataSet;
 import org.apache.iotdb.rpc.IoTDBConnectionException;
@@ -28,6 +33,13 @@ import org.apache.iotdb.rpc.UrlUtils;
 import org.apache.iotdb.session.TableSessionBuilder;
 import org.apache.iotdb.tool.common.Constants;
 
+import org.antlr.v4.runtime.BailErrorStrategy;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.misc.ParseCancellationException;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.exception.write.WriteProcessException;
 import org.apache.tsfile.external.commons.collections4.CollectionUtils;
@@ -51,6 +63,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Collectors;
 
@@ -59,6 +72,7 @@ public class ExportDataTable extends AbstractExportData {
   private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
   private static ITableSession tableSession;
   private static List<String> tables = new ArrayList<>();
+  private static List<String> allTables = new ArrayList<>();
   private static long processedRows;
   private static long lastPrintTime;
 
@@ -85,17 +99,20 @@ public class ExportDataTable extends AbstractExportData {
       System.exit(1);
     }
     sessionDataSet = tableSession.executeQueryStatement("show tables", 
timeout);
+    allTables.clear();
     while (sessionDataSet.hasNext()) {
-      tables.add(sessionDataSet.next().getField(0).getStringValue());
+      allTables.add(sessionDataSet.next().getField(0).getStringValue());
     }
-    if (CollectionUtils.isEmpty(tables)
-        || (ObjectUtils.isNotEmpty(table) && !tables.contains(table))) {
+    boolean filterByTable = StringUtils.isBlank(queryCommand) && 
ObjectUtils.isNotEmpty(table);
+    if (CollectionUtils.isEmpty(allTables) || (filterByTable && 
!allTables.contains(table))) {
       ioTPrinter.println(String.format(Constants.TARGET_TABLE_NOT_EXIST_MSG, 
table));
       System.exit(1);
     }
-    if (ObjectUtils.isNotEmpty(table)) {
-      tables.clear();
+    tables.clear();
+    if (filterByTable) {
       tables.add(table);
+    } else {
+      tables.addAll(allTables);
     }
     if (ObjectUtils.isNotEmpty(sessionDataSet)) {
       sessionDataSet.close();
@@ -103,9 +120,11 @@ public class ExportDataTable extends AbstractExportData {
   }
 
   @Override
-  public void exportBySql(String sql, int index) {
+  public boolean exportBySql(String sql, int index) {
     List<String> exportSql = new ArrayList<>();
-    if (StringUtils.isNotBlank(sql)) {
+    boolean customQuery = StringUtils.isNotBlank(sql);
+    boolean success = true;
+    if (customQuery) {
       if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
           || Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
         legalCheck(sql);
@@ -114,7 +133,7 @@ public class ExportDataTable extends AbstractExportData {
     } else {
       StringBuilder sqlBuilder;
       for (String table : tables) {
-        sqlBuilder = new StringBuilder("select * from ").append(table);
+        sqlBuilder = new StringBuilder("select * from 
").append(quoteIdentifier(table));
         if (StringUtils.isNotBlank(startTime) || 
StringUtils.isNotBlank(endTime)) {
           sqlBuilder.append(" where ");
           if (StringUtils.isNotBlank(startTime)) {
@@ -131,31 +150,108 @@ public class ExportDataTable extends AbstractExportData {
       }
     }
     for (int i = 0; i < exportSql.size(); i++) {
-      String path = targetDirectory + targetFile + i;
-      String table = tables.get(i);
-      try (SessionDataSet sessionDataSet =
-          tableSession.executeQueryStatement(exportSql.get(i), timeout)) {
-        if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)) {
-          exportToSqlFile(sessionDataSet, table, path);
-        } else if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
-          long start = System.currentTimeMillis();
-          boolean isComplete = exportToTsFile(sessionDataSet, path + 
".tsfile", table);
-          if (isComplete) {
-            long end = System.currentTimeMillis();
-            ioTPrinter.println("Export completely!cost: " + (end - start) + " 
ms.");
+      String path = targetDirectory + targetFile + (customQuery ? index : i);
+      try {
+        String queryTable =
+            customQuery
+                ? (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
+                        || 
Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)
+                    ? getQueryTable(sql)
+                    : null)
+                : tables.get(i);
+        try (SessionDataSet sessionDataSet =
+            tableSession.executeQueryStatement(exportSql.get(i), timeout)) {
+          if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)) {
+            exportToSqlFile(sessionDataSet, queryTable, path);
+          } else if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
+            long start = System.currentTimeMillis();
+            boolean isComplete = exportToTsFile(sessionDataSet, path + 
".tsfile", queryTable);
+            if (isComplete) {
+              long end = System.currentTimeMillis();
+              ioTPrinter.println("Export completely!cost: " + (end - start) + 
" ms.");
+            }
+          } else {
+            exportToCsvFile(sessionDataSet, path);
           }
-        } else {
-          exportToCsvFile(sessionDataSet, path);
+          sessionDataSet.closeOperationHandle();
+          ioTPrinter.println(Constants.EXPORT_COMPLETELY);
         }
-        sessionDataSet.closeOperationHandle();
-        ioTPrinter.println(Constants.EXPORT_COMPLETELY);
       } catch (StatementExecutionException
           | IoTDBConnectionException
           | IOException
           | WriteProcessException e) {
         ioTPrinter.println("Cannot dump result because: " + e.getMessage());
+        success = false;
       }
     }
+    return success;
+  }
+
+  private static String getQueryTable(String sql) throws 
StatementExecutionException {
+    String message =
+        String.format(
+            CliMessages
+                
.EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768,
+            database);
+    RelationalSqlLexer lexer =
+        new RelationalSqlLexer(new 
CaseInsensitiveStream(CharStreams.fromString(sql)));
+    RelationalSqlParser parser = new RelationalSqlParser(new 
CommonTokenStream(lexer));
+    lexer.removeErrorListeners();
+    lexer.addErrorListener(
+        new BaseErrorListener() {
+          @Override
+          public void syntaxError(
+              Recognizer<?, ?> recognizer,
+              Object offendingSymbol,
+              int line,
+              int charPositionInLine,
+              String error,
+              RecognitionException cause) {
+            throw new ParseCancellationException(message);
+          }
+        });
+    parser.removeErrorListeners();
+    parser.setErrorHandler(new BailErrorStrategy());
+    List<RelationalSqlParser.QualifiedNameContext> sources = new ArrayList<>();
+    try {
+      new RelationalSqlBaseVisitor<Void>() {
+        @Override
+        public Void visitTableName(RelationalSqlParser.TableNameContext 
context) {
+          sources.add(context.qualifiedName());
+          return null;
+        }
+      }.visit(parser.singleStatement());
+    } catch (ParseCancellationException e) {
+      throw new StatementExecutionException(message);
+    }
+    // Multiple source relations do not have an unambiguous destination table.
+    if (sources.size() != 1) {
+      throw new StatementExecutionException(message);
+    }
+    List<RelationalSqlParser.IdentifierContext> names = 
sources.get(0).identifier();
+    if (names.size() > 2
+        || (names.size() == 2 && 
!identifierValue(names.get(0)).equalsIgnoreCase(database))) {
+      throw new StatementExecutionException(message);
+    }
+    String name = identifierValue(names.get(names.size() - 1));
+    if (!allTables.contains(name)) {
+      throw new StatementExecutionException(message);
+    }
+    return name;
+  }
+
+  private static String identifierValue(RelationalSqlParser.IdentifierContext 
context) {
+    String value = context.getText();
+    if (value.startsWith("\"")) {
+      value = value.substring(1, value.length() - 1).replace("\"\"", "\"");
+    } else if (value.startsWith("`")) {
+      value = value.substring(1, value.length() - 1).replace("``", "`");
+    }
+    return value.toLowerCase(Locale.ROOT);
+  }
+
+  private static String quoteIdentifier(String name) {
+    return "\"" + name.replace("\"", "\"\"") + "\"";
   }
 
   private void exportToSqlFile(SessionDataSet sessionDataSet, String table, 
String filePath)
@@ -164,7 +260,14 @@ public class ExportDataTable extends AbstractExportData {
     lastPrintTime = 0;
     StringBuilder sqlBuilder;
     List<String> headers = sessionDataSet.getColumnNames();
-    String prevSql = "insert into " + table + "(" + StringUtils.join(headers, 
",") + ") values(";
+    String prevSql =
+        "insert into "
+            + quoteIdentifier(table)
+            + "("
+            + headers.stream()
+                .map(ExportDataTable::quoteIdentifier)
+                .collect(Collectors.joining(","))
+            + ") values(";
     SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
     List<String> columnTypeList = iterator.getColumnTypeList();
     int totalColumns = columnTypeList.size();
@@ -185,11 +288,7 @@ public class ExportDataTable extends AbstractExportData {
             }
             String columnType = columnTypeList.get(curColumnIndex);
             String columnValue = iterator.getString(curColumnIndex + 1);
-            if (columnType.equals("TEXT") || columnType.equals("STRING")) {
-              sqlBuilder.append("'").append(columnValue).append("'");
-            } else {
-              sqlBuilder.append(columnValue);
-            }
+            sqlBuilder.append(formatSqlValue(columnValue, columnType));
           }
           sqlBuilder.append(");\n");
           writer.write(sqlBuilder.toString());
@@ -254,7 +353,7 @@ public class ExportDataTable extends AbstractExportData {
       throws IOException, IoTDBConnectionException, 
StatementExecutionException {
     processedRows = 0;
     lastPrintTime = 0;
-    List<String> headers = sessionDataSet.getColumnNames();
+    List<String> headers = getCsvHeaders(sessionDataSet, false);
     int fileIndex = 0;
     SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
     List<String> columnTypeList = iterator.getColumnTypeList();
@@ -363,7 +462,8 @@ public class ExportDataTable extends AbstractExportData {
       List<String> columnNames, Map<String, Integer> deviceColumnIndices, 
String table)
       throws IoTDBConnectionException, StatementExecutionException {
     List<ColumnSchema> columnSchemas = new ArrayList<>();
-    SessionDataSet sessionDataSet = 
tableSession.executeQueryStatement("describe " + table);
+    SessionDataSet sessionDataSet =
+        tableSession.executeQueryStatement("describe " + 
quoteIdentifier(table));
     while (sessionDataSet.hasNext()) {
       RowRecord rowRecord = sessionDataSet.next();
       final String columnName = rowRecord.getField(0).getStringValue();
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
index fd8aeb0cbe3..bab4fa88b92 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
@@ -92,7 +92,7 @@ public class ExportDataTree extends AbstractExportData {
   }
 
   @Override
-  public void exportBySql(String sql, int index) {
+  public boolean exportBySql(String sql, int index) {
     if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
         || Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
       legalCheck(sql);
@@ -109,29 +109,17 @@ public class ExportDataTree extends AbstractExportData {
           ioTPrinter.println("Export completely!cost: " + (end - start) + " 
ms.");
         }
       } else {
-        List<String> headers = new ArrayList<>();
-        List<String> names = sessionDataSet.getColumnNames();
-        List<String> types = sessionDataSet.getColumnTypes();
-        if (Boolean.TRUE.equals(needDataTypePrinted)) {
-          for (int i = 0; i < names.size(); i++) {
-            if (!"Time".equals(names.get(i)) && 
!"Device".equals(names.get(i))) {
-              headers.add(String.format("%s(%s)", names.get(i), types.get(i)));
-            } else {
-              headers.add(names.get(i));
-            }
-          }
-        } else {
-          headers.addAll(names);
-        }
         exportToCsvFile(sessionDataSet, path);
       }
       sessionDataSet.closeOperationHandle();
       ioTPrinter.println(Constants.EXPORT_COMPLETELY);
+      return true;
     } catch (StatementExecutionException
         | IoTDBConnectionException
         | IOException
         | WriteProcessException e) {
       ioTPrinter.println("Cannot dump result because: " + e.getMessage());
+      return false;
     }
   }
 
@@ -140,7 +128,7 @@ public class ExportDataTree extends AbstractExportData {
     processedRows = 0;
     lastPrintTime = 0;
 
-    List<String> measurementNames = sessionDataSet.getColumnNames();
+    List<String> measurementNames = new 
ArrayList<>(sessionDataSet.getColumnNames());
     if (CollectionUtils.isEmpty(measurementNames) || measurementNames.size() 
<= 1) {
       return;
     } else {
@@ -171,12 +159,7 @@ public class ExportDataTree extends AbstractExportData {
       }
       List<String> values = new ArrayList<>();
       for (int index = 2; index < totalColumns; index++) {
-        if ("TEXT".equalsIgnoreCase(columnTypeList.get(index))
-            || "STRING".equalsIgnoreCase(columnTypeList.get(index))) {
-          values.add(String.format("\"%s\"", iterator.getString(index + 1)));
-        } else {
-          values.add(iterator.getString(index + 1));
-        }
+        values.add(formatSqlValue(iterator.getString(index + 1), 
columnTypeList.get(index)));
       }
       long timestamp = iterator.getLong(1);
       writer.write(String.format(sqlPrefix, deviceName, timestamp, 
String.join(",", values)));
@@ -205,73 +188,67 @@ public class ExportDataTree extends AbstractExportData {
       throws IoTDBConnectionException, StatementExecutionException, 
IOException {
     processedRows = 0;
     lastPrintTime = 0;
-
     List<String> headers = sessionDataSet.getColumnNames();
-    List<String> measurementNames = new ArrayList<>();
-    String deviceName = null;
-    if (CollectionUtils.isEmpty(headers) || headers.size() <= 1) {
-      return;
-    } else {
-      headers.remove("Time");
-      Path path = new Path(headers.get(0), true);
-      deviceName = path.getDeviceString();
-      for (String header : headers) {
-        path = new Path(header, true);
-        String meas = path.getMeasurement();
-        if (path.getDeviceString().equals(deviceName)) {
-          measurementNames.add(meas);
-        }
+    Map<String, List<Integer>> deviceColumnIndices = new LinkedHashMap<>();
+    Map<Integer, String> measurements = new HashMap<>();
+    for (int i = 1; i < headers.size(); i++) {
+      Path path = new Path(headers.get(i), true);
+      String device = path.getDeviceString();
+      if (device.startsWith(SYSTEM_DATABASE + ".") || 
device.startsWith(AUDIT_DATABASE + ".")) {
+        continue;
       }
+      deviceColumnIndices.computeIfAbsent(device, key -> new 
ArrayList<>()).add(i);
+      measurements.put(i, path.getMeasurement());
     }
-    if (deviceName.startsWith(SYSTEM_DATABASE + ".")
-        || deviceName.startsWith(AUDIT_DATABASE + ".")) {
-      return;
-    }
-    String sqlPrefix =
-        String.format(
-            "INSERT INTO %s(TIMESTAMP,%s) VALUES (%s);\n",
-            "%s", String.join(",", measurementNames), "%d,%s");
 
     SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
-    List<String> columnTypeList = iterator.getColumnTypeList();
-    int totalColumns = measurementNames.size();
+    List<String> columnTypes = iterator.getColumnTypeList();
     int fileIndex = 0;
     int currentLines = 0;
-    String filePathTemplate = filePath + "_%d" + ".sql";
     FileWriter writer = null;
-    while (iterator.next()) {
-      if (writer == null) {
-        writer = new FileWriter(String.format(filePathTemplate, fileIndex));
-      }
-      List<String> values = new ArrayList<>();
-      for (int index = 0; index < totalColumns; index++) {
-        if ("TEXT".equalsIgnoreCase(columnTypeList.get(index + 1))
-            || "STRING".equalsIgnoreCase(columnTypeList.get(index + 1))) {
-          values.add(String.format("\"%s\"", iterator.getString(index + 2)));
-        } else {
-          values.add(iterator.getString(index + 2));
+    try {
+      while (iterator.next()) {
+        for (Map.Entry<String, List<Integer>> entry : 
deviceColumnIndices.entrySet()) {
+          List<String> names = new ArrayList<>();
+          List<String> values = new ArrayList<>();
+          for (int index : entry.getValue()) {
+            if (!iterator.isNull(index + 1)) {
+              names.add(measurements.get(index));
+              values.add(formatSqlValue(iterator.getString(index + 1), 
columnTypes.get(index)));
+            }
+          }
+          // A timestamp present in another device must not create an empty 
record here.
+          if (values.isEmpty()) {
+            continue;
+          }
+          if (writer == null) {
+            writer = new FileWriter(filePath + "_" + fileIndex + ".sql");
+          }
+          writer.write(
+              String.format(
+                  "INSERT INTO %s(TIMESTAMP,%s) VALUES (%d,%s);\n",
+                  entry.getKey(),
+                  String.join(",", names),
+                  iterator.getLong(1),
+                  String.join(",", values)));
+          if (++currentLines >= linesPerFile) {
+            writer.close();
+            writer = null;
+            fileIndex++;
+            currentLines = 0;
+          }
+        }
+        processedRows++;
+        if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
+          ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
+          lastPrintTime = System.currentTimeMillis();
         }
       }
-      long timestamp = iterator.getLong(1);
-      writer.write(String.format(sqlPrefix, deviceName, timestamp, 
String.join(",", values)));
-      processedRows += 1;
-      currentLines += 1;
-      if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
-        ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
-        lastPrintTime = System.currentTimeMillis();
-      }
-      if (currentLines >= linesPerFile) {
-        writer.flush();
+    } finally {
+      if (writer != null) {
         writer.close();
-        fileIndex += 1;
-        writer = null;
-        currentLines = 0;
       }
     }
-    if (writer != null) {
-      writer.flush();
-      writer.close();
-    }
     ioTPrinter.print("\n");
   }
 
@@ -328,7 +305,7 @@ public class ExportDataTree extends AbstractExportData {
       throws IOException, IoTDBConnectionException, 
StatementExecutionException {
     processedRows = 0;
     lastPrintTime = 0;
-    List<String> headers = sessionDataSet.getColumnNames();
+    List<String> headers = getCsvHeaders(sessionDataSet, true);
     int fileIndex = 0;
     SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
     List<String> columnTypeList = iterator.getColumnTypeList();
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportData.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportData.java
index 9525c171606..8686352cb5a 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportData.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportData.java
@@ -438,8 +438,7 @@ public class ImportData extends AbstractDataTool {
         ioTPrinter.println(String.format("Source file or directory %s does not 
exist", targetPath));
         System.exit(Constants.CODE_ERROR);
       }
-      AbstractImportData.init(importData);
-      return Constants.CODE_OK;
+      return AbstractImportData.init(importData) ? Constants.CODE_OK : 
Constants.CODE_ERROR;
     } catch (InterruptedException e) {
       ioTPrinter.println(String.format("Import tsfile fail: %s", 
e.getMessage()));
       Thread.currentThread().interrupt();
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTable.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTable.java
index f65b559a941..d866efbd242 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTable.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTable.java
@@ -54,6 +54,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -189,6 +190,7 @@ public class ImportDataTable extends AbstractImportData {
   @SuppressWarnings("java:S2259")
   protected void importFromSqlFile(File file) {
     ArrayList<List<Object>> failedRecords = new ArrayList<>();
+    boolean fileFailed = false;
     String failedFilePath;
     if (failedFileDirectory == null) {
       failedFilePath = file.getAbsolutePath() + ".failed";
@@ -210,18 +212,25 @@ public class ImportDataTable extends AbstractImportData {
                     "The extracted database '%s' in SQL statement does not 
match the target database '%s'",
                     dbName, database));
             failedRecords.add(Collections.singletonList(sql));
+            fileFailed = true;
             continue;
           }
           session.executeNonQueryStatement(sql);
         } catch (IoTDBConnectionException | StatementExecutionException e) {
+          fileFailed = true;
           ioTPrinter.println(e.getMessage());
           failedRecords.add(Collections.singletonList(sql));
         }
       }
-      processSuccessFile();
     } catch (IOException e) {
+      fileFailed = true;
       ioTPrinter.println("SQL file read exception because: " + e.getMessage());
     }
+    if (fileFailed) {
+      markFileFailed();
+    } else {
+      processSuccessFile();
+    }
     if (!failedRecords.isEmpty()) {
       FileWriter writer = null;
       try {
@@ -230,6 +239,7 @@ public class ImportDataTable extends AbstractImportData {
           writer.write(failedRecord.get(0).toString() + "\n");
         }
       } catch (IOException e) {
+        markImportFailed();
         ioTPrinter.println("Cannot dump fail result because: " + 
e.getMessage());
       } finally {
         if (ObjectUtils.isNotEmpty(writer)) {
@@ -257,15 +267,16 @@ public class ImportDataTable extends AbstractImportData {
   protected void importFromCsvFile(File file) {
     if (file.getName().endsWith(Constants.CSV_SUFFIXS)
         || file.getName().endsWith(Constants.TXT_SUFFIXS)) {
-      try {
-        CSVParser csvRecords = readCsvFile(file.getAbsolutePath());
+      try (CSVParser csvRecords = readCsvFile(file.getAbsolutePath())) {
         List<String> headerNames = csvRecords.getHeaderNames();
         Stream<CSVRecord> records = csvRecords.stream();
         if (headerNames.isEmpty()) {
+          markFileFailed();
           ioTPrinter.println("Empty file!");
           return;
         }
         if 
(!timeColumn.toLowerCase().equalsIgnoreCase(filterBomHeader(headerNames.get(0))))
 {
+          markFileFailed();
           ioTPrinter.println("The first field of header must be `time`!");
           return;
         }
@@ -275,21 +286,29 @@ public class ImportDataTable extends AbstractImportData {
         } else {
           failedFilePath = failedFileDirectory + file.getName() + ".failed";
         }
-        writeData(headerNames, records.collect(Collectors.toList()), 
failedFilePath);
-        processSuccessFile();
+        if (writeData(headerNames, records.collect(Collectors.toList()), 
failedFilePath)) {
+          processSuccessFile();
+        }
       } catch (IOException e) {
+        markFileFailed();
         ioTPrinter.println("CSV file read exception because: " + 
e.getMessage());
       }
     } else {
+      markFileFailed();
       ioTPrinter.println("The file name must end with \"csv\" or \"txt\"!");
     }
   }
 
-  protected void writeData(
+  protected boolean writeData(
       List<String> headerNames, List<CSVRecord> records, String 
failedFilePath) {
     Map<String, TSDataType> headerTypeMap = new HashMap<>();
     Map<String, String> headerNameMap = new HashMap<>();
-    parseHeaders(headerNames, headerTypeMap, headerNameMap);
+    if (!parseHeaders(headerNames, headerTypeMap, headerNameMap)) {
+      markFileFailed();
+      ioTPrinter.println(
+          
CliMessages.MESSAGE_MULTIPLE_CSV_HEADERS_RESOLVE_TO_THE_SAME_TARGET_COLUMN_99189563);
+      return false;
+    }
     queryType(headerTypeMap);
     List<List<Object>> failedRecords = new ArrayList<>();
     List<IMeasurementSchema> schemas = new ArrayList<>();
@@ -367,41 +386,60 @@ public class ImportDataTable extends AbstractImportData {
     if (!failedRecords.isEmpty()) {
       writeFailedLinesFile(headerNames, failedFilePath, failedRecords);
     }
+    return true;
   }
 
   private static void writeAndEmptyDataSet(Tablet tablet, int retryTime) {
-    try (ITableSession session = sessionPool.getSession()) {
-      session.insert(tablet);
-    } catch (IoTDBConnectionException e) {
-      if (retryTime > 0) {
-        writeAndEmptyDataSet(tablet, --retryTime);
-      }
-    } catch (StatementExecutionException e) {
-      ioTPrinter.println(Constants.INSERT_CSV_MEET_ERROR_MSG + e.getMessage());
-      System.exit(1);
-    }
+    executeWithRetry(
+        () -> {
+          try (ITableSession session = sessionPool.getSession()) {
+            session.insert(tablet);
+          }
+        },
+        retryTime);
   }
 
-  private void parseHeaders(
+  private boolean parseHeaders(
       List<String> headerNames,
       Map<String, TSDataType> headerTypeMap,
       Map<String, String> headerNameMap) {
-    String regex = "(?<=\\()\\S+(?=\\))";
+    String regex = "\\(([^()]+)\\)$";
     Pattern pattern = Pattern.compile(regex);
+    List<String> unmatchedHeaders = new ArrayList<>();
     for (String headerName : headerNames) {
-      headerName = headerName.toLowerCase();
+      String normalizedHeaderName = headerName.toLowerCase(Locale.ROOT);
+      // A complete table column name takes precedence over a possible type 
suffix.
+      if (dataTypes.containsKey(normalizedHeaderName)) {
+        if (headerNameMap.containsKey(normalizedHeaderName)) {
+          return false;
+        }
+        headerNameMap.put(normalizedHeaderName, headerName);
+        headerTypeMap.put(normalizedHeaderName, 
dataTypes.get(normalizedHeaderName));
+      } else {
+        unmatchedHeaders.add(headerName);
+      }
+    }
+    for (String headerName : unmatchedHeaders) {
+      String normalizedHeaderName = headerName.toLowerCase(Locale.ROOT);
       Matcher matcher = pattern.matcher(headerName);
       String type;
       String headerNameWithoutType;
       if (matcher.find()) {
-        type = matcher.group();
-        headerNameWithoutType = headerName.replace("(" + type + ")", 
"").replaceAll("\\s+", "");
+        type = matcher.group(1);
+        headerNameWithoutType = headerName.substring(0, 
matcher.start()).toLowerCase(Locale.ROOT);
+        if (headerNameMap.containsKey(headerNameWithoutType)) {
+          return false;
+        }
         headerNameMap.put(headerNameWithoutType, headerName);
-        headerTypeMap.put(headerNameWithoutType, getType(type));
+        headerTypeMap.put(headerNameWithoutType, 
getType(type.toUpperCase(Locale.ROOT)));
       } else {
-        headerNameMap.put(headerName, headerName);
+        if (headerNameMap.containsKey(normalizedHeaderName)) {
+          return false;
+        }
+        headerNameMap.put(normalizedHeaderName, headerName);
       }
     }
+    return true;
   }
 
   private void queryType(Map<String, TSDataType> dataType) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTree.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTree.java
index c976e847740..35641d3d501 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTree.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ImportDataTree.java
@@ -103,6 +103,7 @@ public class ImportDataTree extends AbstractImportData {
   @SuppressWarnings("java:S2259")
   protected void importFromSqlFile(File file) {
     ArrayList<List<Object>> failedRecords = new ArrayList<>();
+    boolean fileFailed = false;
     String failedFilePath;
     if (failedFileDirectory == null) {
       failedFilePath = file.getAbsolutePath() + ".failed";
@@ -115,19 +116,26 @@ public class ImportDataTree extends AbstractImportData {
         try {
           sessionPool.executeNonQueryStatement(sql);
         } catch (IoTDBConnectionException | StatementExecutionException e) {
+          fileFailed = true;
           failedRecords.add(Collections.singletonList(sql));
         }
       }
-      processSuccessFile(null);
     } catch (IOException e) {
+      fileFailed = true;
       ioTPrinter.println("SQL file read exception because: " + e.getMessage());
     }
+    if (fileFailed) {
+      markFileFailed();
+    } else {
+      processSuccessFile(null);
+    }
     if (!failedRecords.isEmpty()) {
       try (FileWriter writer = new FileWriter(failedFilePath)) {
         for (List<Object> failedRecord : failedRecords) {
           writer.write(failedRecord.get(0).toString() + "\n");
         }
       } catch (IOException e) {
+        markImportFailed();
         ioTPrinter.println("Cannot dump fail result because: " + 
e.getMessage());
       }
     }
@@ -146,15 +154,16 @@ public class ImportDataTree extends AbstractImportData {
   protected void importFromCsvFile(File file) {
     if (file.getName().endsWith(Constants.CSV_SUFFIXS)
         || file.getName().endsWith(Constants.TXT_SUFFIXS)) {
-      try {
-        CSVParser csvRecords = readCsvFile(file.getAbsolutePath());
+      try (CSVParser csvRecords = readCsvFile(file.getAbsolutePath())) {
         List<String> headerNames = csvRecords.getHeaderNames();
         Stream<CSVRecord> records = csvRecords.stream();
         if (headerNames.isEmpty()) {
+          markFileFailed();
           ioTPrinter.println("Empty file!");
           return;
         }
         if (!timeColumn.equalsIgnoreCase(filterBomHeader(headerNames.get(0)))) 
{
+          markFileFailed();
           ioTPrinter.println("The first field of header must be `Time`!");
           return;
         }
@@ -171,9 +180,11 @@ public class ImportDataTree extends AbstractImportData {
         }
         processSuccessFile(null);
       } catch (IOException | IllegalPathException e) {
+        markFileFailed();
         ioTPrinter.println("CSV file read exception because: " + 
e.getMessage());
       }
     } else {
+      markFileFailed();
       ioTPrinter.println("The file name must end with \"csv\" or \"txt\"!");
     }
   }
@@ -401,26 +412,21 @@ public class ImportDataTree extends AbstractImportData {
       List<List<Object>> valuesList,
       List<List<String>> measurementsList,
       int retryTime) {
-    try {
-      if (Boolean.FALSE.equals(aligned)) {
-        sessionPool.insertRecordsOfOneDevice(
-            device, times, measurementsList, typesList, valuesList);
-      } else {
-        sessionPool.insertAlignedRecordsOfOneDevice(
-            device, times, measurementsList, typesList, valuesList);
-      }
-    } catch (IoTDBConnectionException e) {
-      if (retryTime > 0) {
-        writeAndEmptyDataSet(device, times, typesList, valuesList, 
measurementsList, --retryTime);
-      }
-    } catch (StatementExecutionException e) {
-      ioTPrinter.println(Constants.INSERT_CSV_MEET_ERROR_MSG + e.getMessage());
-    } finally {
-      times.clear();
-      typesList.clear();
-      valuesList.clear();
-      measurementsList.clear();
-    }
+    executeWithRetry(
+        () -> {
+          if (Boolean.FALSE.equals(aligned)) {
+            sessionPool.insertRecordsOfOneDevice(
+                device, times, measurementsList, typesList, valuesList);
+          } else {
+            sessionPool.insertAlignedRecordsOfOneDevice(
+                device, times, measurementsList, typesList, valuesList);
+          }
+        },
+        retryTime);
+    times.clear();
+    typesList.clear();
+    valuesList.clear();
+    measurementsList.clear();
   }
 
   private static void writeAndEmptyDataSet(
@@ -430,27 +436,21 @@ public class ImportDataTree extends AbstractImportData {
       List<List<Object>> valuesList,
       List<List<String>> measurementsList,
       int retryTime) {
-    try {
-      if (Boolean.FALSE.equals(aligned)) {
-        sessionPool.insertRecords(deviceIds, times, measurementsList, 
typesList, valuesList);
-      } else {
-        sessionPool.insertAlignedRecords(deviceIds, times, measurementsList, 
typesList, valuesList);
-      }
-    } catch (IoTDBConnectionException e) {
-      if (retryTime > 0) {
-        writeAndEmptyDataSet(
-            deviceIds, times, typesList, valuesList, measurementsList, 
--retryTime);
-      }
-    } catch (StatementExecutionException e) {
-      ioTPrinter.println(Constants.INSERT_CSV_MEET_ERROR_MSG + e.getMessage());
-      System.exit(1);
-    } finally {
-      deviceIds.clear();
-      times.clear();
-      typesList.clear();
-      valuesList.clear();
-      measurementsList.clear();
-    }
+    executeWithRetry(
+        () -> {
+          if (Boolean.FALSE.equals(aligned)) {
+            sessionPool.insertRecords(deviceIds, times, measurementsList, 
typesList, valuesList);
+          } else {
+            sessionPool.insertAlignedRecords(
+                deviceIds, times, measurementsList, typesList, valuesList);
+          }
+        },
+        retryTime);
+    deviceIds.clear();
+    times.clear();
+    typesList.clear();
+    valuesList.clear();
+    measurementsList.clear();
   }
 
   private static void queryType(
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFile.java 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFile.java
index 97ff8d164f3..3b9e36a7965 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFile.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFile.java
@@ -370,8 +370,7 @@ public class ImportTsFile extends AbstractTsFileTool {
       ImportTsFileScanTool.traverseAndCollectFiles();
       ImportTsFileScanTool.addNoResourceOrModsToQueue();
 
-      asyncImportTsFiles();
-      return CODE_OK;
+      return asyncImportTsFiles();
     } catch (InterruptedException e) {
       IOT_PRINTER.println(String.format("Import tsfile fail: %s", 
e.getMessage()));
       Thread.currentThread().interrupt();
@@ -413,7 +412,8 @@ public class ImportTsFile extends AbstractTsFileTool {
         successDir, successOperation, failDir, failOperation);
   }
 
-  public static void asyncImportTsFiles() {
+  public static int asyncImportTsFiles() {
+    ImportTsFileBase.resetCounters();
     final List<Thread> list = new ArrayList<>(threadNum);
     for (int i = 0; i < threadNum; i++) {
       final Thread thread =
@@ -433,5 +433,8 @@ public class ImportTsFile extends AbstractTsFileTool {
             IOT_PRINTER.println("ImportTsFile thread join interrupted: " + 
e.getMessage());
           }
         });
+    return Thread.currentThread().isInterrupted() || 
ImportTsFileBase.hasFailures()
+        ? CODE_ERROR
+        : CODE_OK;
   }
 }
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileBase.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileBase.java
index f1fd3bf2d30..634eda9741d 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileBase.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileBase.java
@@ -22,12 +22,9 @@ package org.apache.iotdb.tool.tsfile;
 import org.apache.iotdb.cli.utils.IoTPrinter;
 import org.apache.iotdb.tool.common.ImportTsFileOperation;
 
-import java.io.File;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.Files;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.util.Objects;
 import java.util.concurrent.atomic.LongAdder;
 
@@ -101,107 +98,26 @@ public abstract class ImportTsFileBase implements 
Runnable {
     }
   }
 
-  public static void processingFile(final String filePath, final boolean 
isSuccess) {
-    final String relativePath =
-        filePath.substring(ImportTsFileScanTool.getSourceFullPathLength() + 1);
-    final Path sourcePath = Paths.get(filePath);
-
-    final String target =
-        isSuccess
-            ? successDir
-            : failDir + File.separator + relativePath.replace(File.separator, 
"_");
-    final Path targetPath = Paths.get(target);
-
-    final String RESOURCE = ".resource";
-    Path sourceResourcePath = Paths.get(sourcePath + RESOURCE);
-    sourceResourcePath = Files.exists(sourceResourcePath) ? sourceResourcePath 
: null;
-    final Path targetResourcePath = Paths.get(target + RESOURCE);
-
-    final String MODS = ".mods";
-    Path sourceModsPath = Paths.get(sourcePath + MODS);
-    sourceModsPath = Files.exists(sourceModsPath) ? sourceModsPath : null;
-    final Path targetModsPath = Paths.get(target + MODS);
-
-    switch (isSuccess ? successOperation : failOperation) {
-      case DELETE:
-        {
-          try {
-            Files.deleteIfExists(sourcePath);
-            if (null != sourceResourcePath) {
-              Files.deleteIfExists(sourceResourcePath);
-            }
-            if (null != sourceModsPath) {
-              Files.deleteIfExists(sourceModsPath);
-            }
-          } catch (final Exception e) {
-            IOT_PRINTER.println(String.format("Failed to delete file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case CP:
-        {
-          try {
-            Files.copy(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            if (null != sourceResourcePath) {
-              Files.copy(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.copy(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            IOT_PRINTER.println(String.format("Failed to copy file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case HARDLINK:
-        {
-          try {
-            Files.createLink(targetPath, sourcePath);
-          } catch (FileAlreadyExistsException e) {
-            IOT_PRINTER.println("Hardlink already exists: " + e.getMessage());
-          } catch (final Exception e) {
-            try {
-              Files.copy(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            } catch (final Exception copyException) {
-              IOT_PRINTER.println(
-                  String.format("Failed to copy file: %s", 
copyException.getMessage()));
-            }
-          }
-
-          try {
-            if (null != sourceResourcePath) {
-              Files.copy(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.copy(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            IOT_PRINTER.println(
-                String.format("Failed to copy resource or mods file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      case MV:
-        {
-          try {
-            Files.move(sourcePath, targetPath, 
StandardCopyOption.REPLACE_EXISTING);
-            if (null != sourceResourcePath) {
-              Files.move(
-                  sourceResourcePath, targetResourcePath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-            if (null != sourceModsPath) {
-              Files.move(sourceModsPath, targetModsPath, 
StandardCopyOption.REPLACE_EXISTING);
-            }
-          } catch (final Exception e) {
-            IOT_PRINTER.println(String.format("Failed to move file: %s", 
e.getMessage()));
-          }
-          break;
-        }
-      default:
-        break;
-    }
+  public static void processingFile(final String filePath, final boolean 
isSuccess)
+      throws IOException {
+    final Path target =
+        Paths.get(isSuccess ? successDir : failDir)
+            .resolve(ImportTsFileScanTool.getRelativePath(filePath));
+    final ImportTsFileOperation operation = isSuccess ? successOperation : 
failOperation;
+    operation.processFileGroup(Paths.get(filePath), target);
+  }
+
+  protected static void resetCounters() {
+    loadFileSuccessfulNum.reset();
+    loadFileFailedNum.reset();
+    processingLoadSuccessfulFileSuccessfulNum.reset();
+    processingLoadFailedFileSuccessfulNum.reset();
+  }
+
+  protected static boolean hasFailures() {
+    return loadFileFailedNum.sum() > 0
+        || loadFileSuccessfulNum.sum() != 
processingLoadSuccessfulFileSuccessfulNum.sum()
+        || ImportTsFileScanTool.getTsFileQueueSize() > 0;
   }
 
   protected static void printResult(final long startTime) {
diff --git 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileScanTool.java
 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileScanTool.java
index 4891f425486..f31e7bc72e9 100644
--- 
a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileScanTool.java
+++ 
b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/tsfile/ImportTsFileScanTool.java
@@ -20,6 +20,8 @@
 package org.apache.iotdb.tool.tsfile;
 
 import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -91,6 +93,12 @@ public class ImportTsFileScanTool {
         : new File(ImportTsFileScanTool.sourceFullPath).getParent().length();
   }
 
+  public static String getRelativePath(String file) {
+    Path source = Paths.get(sourceFullPath).toAbsolutePath().normalize();
+    Path directory = source.toFile().isDirectory() ? source : 
source.getParent();
+    return 
directory.relativize(Paths.get(file).toAbsolutePath().normalize()).toString();
+  }
+
   public static int getTsFileQueueSize() {
     return ImportTsFileScanTool.tsfileQueue.size();
   }
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
new file mode 100644
index 00000000000..ff4a59ada91
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
@@ -0,0 +1,1042 @@
+/*
+ * 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.tool.data;
+
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.ITableSessionPool;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.rpc.StatementExecutionException;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.session.pool.SessionPool;
+import org.apache.iotdb.tool.common.ImportTsFileOperation;
+import org.apache.iotdb.tool.tsfile.ImportTsFileBase;
+import org.apache.iotdb.tool.tsfile.ImportTsFileScanTool;
+
+import org.apache.commons.csv.CSVParser;
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.write.record.Tablet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DataToolRegressionTest extends AbstractDataTool {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private final Map<Field, Object> originalFields = new HashMap<>();
+  private Path directory;
+
+  @Before
+  public void setUp() throws Exception {
+    for (Class<?> owner :
+        Arrays.asList(
+            AbstractDataTool.class,
+            ExportDataTree.class,
+            ExportDataTable.class,
+            ImportDataTree.class,
+            ImportDataTable.class,
+            ImportTsFileBase.class,
+            ImportTsFileScanTool.class)) {
+      for (Field field : owner.getDeclaredFields()) {
+        if (Modifier.isStatic(field.getModifiers()) && 
!Modifier.isFinal(field.getModifiers())) {
+          field.setAccessible(true);
+          originalFields.put(field, field.get(null));
+        }
+      }
+    }
+    directory = temporaryFolder.getRoot().toPath();
+    targetDirectory = directory + File.separator;
+    targetFile = "dump";
+    exportType = "sql";
+    fileType = "csv";
+    timeFormat = "timestamp";
+    timeColumn = "Time";
+    timestampPrecision = "ms";
+    database = "db";
+    table = "t";
+    aligned = false;
+    linesPerFile = 10000;
+    batchPointSize = 100;
+    threadNum = 1;
+    needDataTypePrinted = false;
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
+      entry.getKey().set(null, entry.getValue());
+    }
+  }
+
+  @Test
+  public void testMultiDeviceSqlPreservesColumnMappingAndSplitsFiles() throws 
Exception {
+    linesPerFile = 1;
+    mockTree(
+        dataSet(
+            List.of("Time", "root.a.d1.s1", "root.a.d2.s2", "root.a.d1.s3"),
+            List.of("INT64", "INT32", "INT32", "INT32"),
+            new Object[] {1L, 11, 22, 33},
+            new Object[] {2L, null, 44, null}));
+    assertTrue(new ExportDataTree().exportBySql("select * from root.a.*", 0));
+    assertEquals("INSERT INTO root.a.d1(TIMESTAMP,s1,s3) VALUES (1,11,33);\n", 
read("dump0_0.sql"));
+    assertEquals("INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (1,22);\n", 
read("dump0_1.sql"));
+    assertEquals("INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (2,44);\n", 
read("dump0_2.sql"));
+    assertFalse(Files.exists(directory.resolve("dump0_3.sql")));
+  }
+
+  @Test
+  public void testCustomTableQueriesUseDistinctFilesAndActualTables() throws 
Exception {
+    ITableSession session = mockTable("table_a", "table_b");
+    SessionDataSet first =
+        dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new 
Object[] {1L, 11});
+    SessionDataSet second =
+        dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new 
Object[] {2L, 22});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(first, second);
+    ExportDataTable tool = new ExportDataTable();
+    assertTrue(tool.exportBySql("select * from table_a", 0));
+    assertTrue(tool.exportBySql("SELECT * FROM \"db\".\"table_b\" WHERE tag = 
'from table_a'", 1));
+    assertEquals("insert into \"table_a\"(\"time\",\"v\") values(1,11);\n", 
read("dump0_0.sql"));
+    assertEquals("insert into \"table_b\"(\"time\",\"v\") values(2,22);\n", 
read("dump1_0.sql"));
+  }
+
+  @Test
+  public void testCustomTableQueryOverridesTableFilter() throws Exception {
+    ITableSession session = mockTable("table_a", "table_b");
+    table = "table_a";
+    set(ExportDataTable.class, "tables", new ArrayList<>(List.of("table_a")));
+    SessionDataSet result =
+        dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new 
Object[] {1L, 11});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+
+    assertTrue(new ExportDataTable().exportBySql("select * from table_b", 0));
+    assertEquals("insert into \"table_b\"(\"time\",\"v\") values(1,11);\n", 
read("dump0_0.sql"));
+  }
+
+  @Test
+  public void testDefaultTableExportStillUsesTableFilter() throws Exception {
+    ITableSession session = mockTable("table_a", "table_b");
+    table = "table_a";
+    set(ExportDataTable.class, "tables", new ArrayList<>(List.of("table_a")));
+    SessionDataSet result =
+        dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new 
Object[] {1L, 11});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+
+    assertTrue(new ExportDataTable().exportBySql(null, 0));
+    verify(session).executeQueryStatement("select * from \"table_a\"", 
timeout);
+    assertEquals("insert into \"table_a\"(\"time\",\"v\") values(1,11);\n", 
read("dump0_0.sql"));
+    assertFalse(Files.exists(directory.resolve("dump1_0.sql")));
+  }
+
+  @Test
+  public void testQuotedTableAndColumnNames() throws Exception {
+    ITableSession session = mockTable("odd\"name");
+    SessionDataSet result =
+        dataSet(List.of("time", "select"), List.of("TIMESTAMP", "INT32"), new 
Object[] {1L, 11});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+    assertTrue(new ExportDataTable().exportBySql("select * from 
\"ODD\"\"NAME\"", 0));
+    assertEquals(
+        "insert into \"odd\"\"name\"(\"time\",\"select\") values(1,11);\n", 
read("dump0_0.sql"));
+  }
+
+  @Test
+  public void testAmbiguousAndCrossDatabaseTableQueriesFailBeforeWriting() 
throws Exception {
+    ITableSession session = mockTable("table_a", "table_b");
+    ExportDataTable tool = new ExportDataTable();
+    assertFalse(
+        tool.exportBySql("select * from table_a join table_b on table_a.time = 
table_b.time", 0));
+    assertFalse(tool.exportBySql("select * from other_db.table_b", 1));
+    assertFalse(tool.exportBySql("select * from", 2));
+    assertFalse(tool.exportBySql("select * from table_b #invalid_character", 
3));
+    verify(session, never()).executeQueryStatement(anyString(), anyLong());
+    assertFalse(Files.exists(directory.resolve("dump0_0.sql")));
+  }
+
+  @Test
+  public void testTableSqlEscapesStringsAndPreservesNull() throws Exception {
+    ITableSession session = mockTable("t");
+    SessionDataSet result =
+        dataSet(
+            List.of("time", "tag", "value"),
+            List.of("TIMESTAMP", "STRING", "TEXT"),
+            new Object[] {1L, "O'Brien\\path", null});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+    assertTrue(new ExportDataTable().exportBySql("select * from t", 0));
+    assertEquals(
+        "insert into \"t\"(\"time\",\"tag\",\"value\") 
values(1,'O''Brien\\path',NULL);\n",
+        read("dump0_0.sql"));
+  }
+
+  @Test
+  public void testAlignedTreeSqlEscapesStringsAndPreservesNull() throws 
Exception {
+    mockTree(
+        dataSet(
+            List.of("Time", "Device", "s1", "s2"),
+            List.of("INT64", "TEXT", "TEXT", "STRING"),
+            new Object[] {1L, "root.a.d1", "a'b\"\\c", null}));
+    assertTrue(new ExportDataTree().exportBySql("select * from root.a.* align 
by device", 0));
+    assertEquals(
+        "INSERT INTO root.a.d1(TIMESTAMP,s1,s2) ALIGNED VALUES 
(1,'a''b\"\\c',NULL);\n",
+        read("dump0_0.sql"));
+  }
+
+  @Test
+  public void testTreeCsvTypeHeadersInEverySplitFile() throws Exception {
+    exportType = "csv";
+    linesPerFile = 1;
+    needDataTypePrinted = true;
+    mockTree(
+        dataSet(
+            List.of("Time", "Device", "s1"),
+            List.of("INT64", "TEXT", "DOUBLE"),
+            new Object[] {1L, "root.a.d1", 1.5},
+            new Object[] {2L, "root.a.d1", 2.5}));
+    assertTrue(new ExportDataTree().exportBySql("select * from root.a.* align 
by device", 0));
+    assertEquals("Time,Device,s1(DOUBLE)", firstLine("dump0_0.csv"));
+    assertEquals("Time,Device,s1(DOUBLE)", firstLine("dump0_1.csv"));
+    needDataTypePrinted = false;
+    mockTree(
+        dataSet(
+            List.of("Time", "root.a.d1.s1"), List.of("INT64", "DOUBLE"), new 
Object[] {1L, 1.5}));
+    assertTrue(new ExportDataTree().exportBySql("select * from root.a.d1", 1));
+    assertEquals("Time,root.a.d1.s1", firstLine("dump1_0.csv"));
+  }
+
+  @Test
+  public void testTableCsvTypeHeadersAndMultipleQueryFiles() throws Exception {
+    exportType = "csv";
+    needDataTypePrinted = true;
+    ITableSession session = mockTable("t");
+    SessionDataSet first =
+        dataSet(
+            List.of("time", "device", "v"),
+            List.of("TIMESTAMP", "STRING", "DOUBLE"),
+            new Object[] {1L, "sensor", 1.5});
+    SessionDataSet second =
+        dataSet(List.of("time", "v"), List.of("TIMESTAMP", "DOUBLE"), new 
Object[] {2L, 2.5});
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(first, second);
+    ExportDataTable tool = new ExportDataTable();
+    assertTrue(tool.exportBySql("select * from t", 0));
+    needDataTypePrinted = false;
+    assertTrue(tool.exportBySql("select time,v from t", 1));
+    assertEquals("time,device(STRING),v(DOUBLE)", firstLine("dump0_0.csv"));
+    assertEquals("time,v", firstLine("dump1_0.csv"));
+  }
+
+  @Test
+  public void testExportQueryFailureIsReported() throws Exception {
+    Session session = mock(Session.class);
+    when(session.executeQueryStatement(anyString(), anyLong()))
+        .thenThrow(new IoTDBConnectionException("disconnect"));
+    set(ExportDataTree.class, "session", session);
+    assertFalse(new ExportDataTree().exportBySql("select * from root.a.d1", 
0));
+  }
+
+  @Test
+  public void testOneDeviceRetriesRetainFailedBatch() throws Exception {
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    List<Long> timestamps = new ArrayList<>(List.of(1L));
+    List<List<TSDataType>> types = new 
ArrayList<>(List.of(List.of(TSDataType.INT32)));
+    List<List<Object>> values = new ArrayList<>(List.of(List.of(11)));
+    List<List<String>> measurements = new ArrayList<>(List.of(List.of("s1")));
+    IoTDBConnectionException failure = new 
IoTDBConnectionException("disconnect");
+    doThrow(failure)
+        .when(pool)
+        .insertRecordsOfOneDevice("root.a.d1", timestamps, measurements, 
types, values);
+    try {
+      invoke(
+          ImportDataTree.class,
+          "writeAndEmptyDataSet",
+          null,
+          new Class<?>[] {String.class, List.class, List.class, List.class, 
List.class, int.class},
+          "root.a.d1",
+          timestamps,
+          types,
+          values,
+          measurements,
+          3);
+      fail("Expected exhausted retries to fail");
+    } catch (IllegalStateException e) {
+      assertSame(failure, e.getCause());
+    }
+    verify(pool, times(4))
+        .insertRecordsOfOneDevice("root.a.d1", timestamps, measurements, 
types, values);
+    assertEquals(List.of(1L), timestamps);
+    assertEquals(List.of(List.of(11)), values);
+    assertFalse(types.isEmpty());
+    assertFalse(measurements.isEmpty());
+  }
+
+  @Test
+  public void testOneDeviceSuccessfulRetryClearsBatchOnlyAfterInsert() throws 
Exception {
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    List<Long> timestamps = new ArrayList<>(List.of(1L));
+    List<List<TSDataType>> types = new 
ArrayList<>(List.of(List.of(TSDataType.INT32)));
+    List<List<Object>> values = new ArrayList<>(List.of(List.of(11)));
+    List<List<String>> measurements = new ArrayList<>(List.of(List.of("s1")));
+    AtomicInteger attempts = new AtomicInteger();
+    doAnswer(
+            invocation -> {
+              assertEquals(List.of(1L), timestamps);
+              assertEquals(List.of(List.of(11)), values);
+              if (attempts.incrementAndGet() < 3) {
+                throw new IoTDBConnectionException("disconnect");
+              }
+              return null;
+            })
+        .when(pool)
+        .insertRecordsOfOneDevice("root.a.d1", timestamps, measurements, 
types, values);
+    invoke(
+        ImportDataTree.class,
+        "writeAndEmptyDataSet",
+        null,
+        new Class<?>[] {String.class, List.class, List.class, List.class, 
List.class, int.class},
+        "root.a.d1",
+        timestamps,
+        types,
+        values,
+        measurements,
+        3);
+    assertEquals(3, attempts.get());
+    assertTrue(timestamps.isEmpty());
+    assertTrue(values.isEmpty());
+    assertTrue(types.isEmpty());
+    assertTrue(measurements.isEmpty());
+  }
+
+  @Test
+  public void testAlignedMultiDeviceRetriesRetainFailedBatch() throws 
Exception {
+    aligned = true;
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    List<String> devices = new ArrayList<>(List.of("root.a.d1"));
+    List<Long> timestamps = new ArrayList<>(List.of(1L));
+    List<List<TSDataType>> types = new 
ArrayList<>(List.of(List.of(TSDataType.INT32)));
+    List<List<Object>> values = new ArrayList<>(List.of(List.of(11)));
+    List<List<String>> measurements = new ArrayList<>(List.of(List.of("s1")));
+    doThrow(new IoTDBConnectionException("disconnect"))
+        .when(pool)
+        .insertAlignedRecords(devices, timestamps, measurements, types, 
values);
+    try {
+      invoke(
+          ImportDataTree.class,
+          "writeAndEmptyDataSet",
+          null,
+          new Class<?>[] {List.class, List.class, List.class, List.class, 
List.class, int.class},
+          devices,
+          timestamps,
+          types,
+          values,
+          measurements,
+          3);
+      fail("Expected exhausted retries to fail");
+    } catch (IllegalStateException expected) {
+      assertTrue(expected.getCause() instanceof IoTDBConnectionException);
+    }
+    verify(pool, times(4)).insertAlignedRecords(devices, timestamps, 
measurements, types, values);
+    assertEquals(List.of("root.a.d1"), devices);
+    assertEquals(List.of(1L), timestamps);
+    assertEquals(List.of(List.of(11)), values);
+  }
+
+  @Test
+  public void testTableRetriesRetainTabletAndCloseSessions() throws Exception {
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mock(ITableSession.class);
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    Tablet tablet =
+        new Tablet(
+            "t", List.of("v"), List.of(TSDataType.DOUBLE), 
List.of(ColumnCategory.FIELD), 100);
+    tablet.addTimestamp(0, 1L);
+    tablet.addValue("v", 0, 1.5);
+    doThrow(new 
IoTDBConnectionException("disconnect")).when(session).insert(tablet);
+    try {
+      invoke(
+          ImportDataTable.class,
+          "writeAndEmptyDataSet",
+          null,
+          new Class<?>[] {Tablet.class, int.class},
+          tablet,
+          3);
+      fail("Expected exhausted retries to fail");
+    } catch (IllegalStateException expected) {
+      assertTrue(expected.getCause() instanceof IoTDBConnectionException);
+    }
+    assertEquals(1, tablet.getRowSize());
+    verify(session, times(4)).insert(tablet);
+    verify(session, times(4)).close();
+  }
+
+  @Test
+  public void testWorkerFailurePreventsSuccessAndResetsForNextImport() throws 
Exception {
+    Path csv = directory.resolve("input.csv");
+    Files.writeString(csv, "Time,root.a.d1.s1(INT32)\n1,11\n");
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    doThrow(new IoTDBConnectionException("disconnect"))
+        .when(pool)
+        .insertRecords(any(), any(), any(), any(), any());
+    long successes = loadFileSuccessfulNum.sum();
+    ImportDataScanTool.putToQueue(csv.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTree()));
+    assertEquals(successes, loadFileSuccessfulNum.sum());
+    doNothing().when(pool).insertRecords(any(), any(), any(), any(), any());
+    ImportDataScanTool.putToQueue(csv.toString());
+    assertTrue(AbstractImportData.init(new ImportDataTree()));
+    assertEquals(successes + 1, loadFileSuccessfulNum.sum());
+  }
+
+  @Test
+  public void testFailedCsvBatchDoesNotSkipNextFile() throws Exception {
+    Path bad = directory.resolve("a_bad.csv");
+    Path good = directory.resolve("b_good.csv");
+    Files.writeString(bad, "Time,root.a.d1.s1(INT32)\n1,11\n");
+    Files.writeString(good, "Time,root.a.d2.s1(INT32)\n1,22\n");
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    AtomicInteger goodInserted = new AtomicInteger();
+    doAnswer(
+            invocation -> {
+              if (((List<?>) invocation.getArgument(0)).contains("root.a.d1")) 
{
+                throw new StatementExecutionException("type mismatch");
+              }
+              if (((List<?>) invocation.getArgument(0)).contains("root.a.d2")) 
{
+                goodInserted.incrementAndGet();
+              }
+              return null;
+            })
+        .when(pool)
+        .insertRecords(any(), any(), any(), any(), any());
+    ImportDataScanTool.putToQueue(bad.toString());
+    ImportDataScanTool.putToQueue(good.toString());
+
+    assertFalse(AbstractImportData.init(new ImportDataTree()));
+
+    assertEquals(1, goodInserted.get());
+    assertNull(ImportDataScanTool.pollFromQueue());
+  }
+
+  @Test
+  public void testRejectedCsvAndFailedSqlAffectImportExitStatus() throws 
Exception {
+    Path badCsv = directory.resolve("bad_header.csv");
+    Files.writeString(badCsv, "ts,root.a.d1.s1(INT32)\n1,11\n");
+    ImportDataScanTool.putToQueue(badCsv.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTree()));
+
+    Path badTableCsv = directory.resolve("bad_table_header.csv");
+    Files.writeString(badTableCsv, "ts,v\n1,11\n");
+    ImportDataScanTool.putToQueue(badTableCsv.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTable()));
+
+    Path failedTreeSql = directory.resolve("failed_tree.sql");
+    Files.writeString(failedTreeSql, "insert into root.a.d1(time,s1) 
values(1,11)\n");
+    SessionPool treePool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", treePool);
+    doThrow(new StatementExecutionException("statement failed"))
+        .when(treePool)
+        .executeNonQueryStatement(anyString());
+    ImportDataScanTool.putToQueue(failedTreeSql.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTree()));
+
+    Path failedTableSql = directory.resolve("failed_table.sql");
+    Files.writeString(failedTableSql, "insert into db.t(time,v) 
values(1,11)\n");
+    ITableSessionPool tablePool = mock(ITableSessionPool.class);
+    ITableSession tableSession = mock(ITableSession.class);
+    when(tablePool.getSession()).thenReturn(tableSession);
+    doThrow(new StatementExecutionException("statement failed"))
+        .when(tableSession)
+        .executeNonQueryStatement(anyString());
+    set(ImportDataTable.class, "sessionPool", tablePool);
+    ImportDataScanTool.putToQueue(failedTableSql.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTable()));
+  }
+
+  @Test
+  public void testRejectedFilesAreCountedOnceInBothModels() throws Exception {
+    Path empty = directory.resolve("empty.csv");
+    Path header = directory.resolve("invalid.csv");
+    Path extension = directory.resolve("input.other");
+    Files.writeString(empty, "");
+    Files.writeString(header, "ts,v\n1,11\n");
+    Files.writeString(extension, "Time,v\n1,11\n");
+    for (AbstractImportData importer : List.of(new ImportDataTree(), new 
ImportDataTable())) {
+      for (Path source :
+          List.of(
+              empty,
+              header,
+              extension,
+              directory.resolve("missing.csv"),
+              directory.resolve("missing.sql"))) {
+        long failures = loadFileFailedNum.sum();
+        long successes = loadFileSuccessfulNum.sum();
+        ImportDataScanTool.putToQueue(source.toString());
+        assertFalse(AbstractImportData.init(importer));
+        assertEquals(failures + 1, loadFileFailedNum.sum());
+        assertEquals(successes, loadFileSuccessfulNum.sum());
+      }
+    }
+  }
+
+  @Test
+  public void testFailedDeviceCsvDoesNotSkipNextFile() throws Exception {
+    Path bad = directory.resolve("a_bad.csv");
+    Path good = directory.resolve("b_good.csv");
+    Files.writeString(bad, "Time,Device,s1(INT32)\n1,root.a.d1,11\n");
+    Files.writeString(good, "Time,Device,s1(INT32)\n1,root.a.d2,22\n");
+    SessionPool pool = mock(SessionPool.class);
+    set(ImportDataTree.class, "sessionPool", pool);
+    AtomicInteger goodInserted = new AtomicInteger();
+    doAnswer(
+            invocation -> {
+              if ("root.a.d1".equals(invocation.getArgument(0))) {
+                throw new StatementExecutionException("type mismatch");
+              }
+              goodInserted.incrementAndGet();
+              return null;
+            })
+        .when(pool)
+        .insertRecordsOfOneDevice(anyString(), any(), any(), any(), any());
+    ImportDataScanTool.putToQueue(bad.toString());
+    ImportDataScanTool.putToQueue(good.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTree()));
+    assertEquals(1, goodInserted.get());
+    assertNull(ImportDataScanTool.pollFromQueue());
+  }
+
+  @Test
+  public void testFailedTableCsvDoesNotSkipNextFile() throws Exception {
+    Path bad = directory.resolve("a_bad.csv");
+    Path good = directory.resolve("b_good.csv");
+    Files.writeString(bad, "Time,v(INT32)\n1,11\n");
+    Files.writeString(good, "Time,v(INT32)\n2,22\n");
+    set(ImportDataTable.class, "dataTypes", Map.of("v", TSDataType.INT32));
+    set(ImportDataTable.class, "columnCategory", Map.of("v", 
ColumnCategory.FIELD));
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mock(ITableSession.class);
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    AtomicInteger goodInserted = new AtomicInteger();
+    doAnswer(
+            invocation -> {
+              Tablet tablet = invocation.getArgument(0);
+              if (tablet.getTimestamps()[0] == 1L) {
+                throw new StatementExecutionException("type mismatch");
+              }
+              goodInserted.incrementAndGet();
+              return null;
+            })
+        .when(session)
+        .insert(any(Tablet.class));
+    ImportDataScanTool.putToQueue(bad.toString());
+    ImportDataScanTool.putToQueue(good.toString());
+    assertFalse(AbstractImportData.init(new ImportDataTable()));
+    assertEquals(1, goodInserted.get());
+    assertNull(ImportDataScanTool.pollFromQueue());
+  }
+
+  @Test
+  public void testTypedTableCsvUsesOriginalHeaderSpelling() throws Exception {
+    Map<String, TSDataType> types = new LinkedHashMap<>();
+    types.put("id", TSDataType.STRING);
+    types.put("v", TSDataType.DOUBLE);
+    Map<String, ColumnCategory> categories = new LinkedHashMap<>();
+    categories.put("id", ColumnCategory.TAG);
+    categories.put("v", ColumnCategory.FIELD);
+    set(ImportDataTable.class, "dataTypes", types);
+    set(ImportDataTable.class, "columnCategory", categories);
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mock(ITableSession.class);
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    Path csv = directory.resolve("typed.csv");
+    Files.writeString(csv, "TIME,ID(STRING),v(double)\n1,sensor,1.5\n");
+    try (CSVParser records = readCsvFile(csv.toString())) {
+      new ImportDataTable()
+          .writeData(
+              records.getHeaderNames(),
+              records.getRecords(),
+              directory.resolve("typed.failed").toString());
+    }
+    verify(session).insert(any(Tablet.class));
+    assertFalse(Files.exists(directory.resolve("typed.failed")));
+  }
+
+  @Test
+  public void 
testTableCsvRoundTripPreservesWhitespaceAndParenthesesInColumnNames()
+      throws Exception {
+    assertTableCsvRoundTrip("value(double) with space");
+  }
+
+  @Test
+  public void testTableCsvRoundTripPreservesTrailingParenthesesInColumnNames() 
throws Exception {
+    for (String column : List.of("value(foo)", "value(double)", 
"value(foo)(double)")) {
+      assertTableCsvRoundTrip(column);
+    }
+  }
+
+  @Test
+  public void testTableCsvPrefersCompleteColumnNameOverTypeSuffix() throws 
Exception {
+    Map<String, TSDataType> types = new LinkedHashMap<>();
+    types.put("value", TSDataType.DOUBLE);
+    types.put("value(double)", TSDataType.DOUBLE);
+    Map<String, ColumnCategory> categories = new LinkedHashMap<>();
+    categories.put("value", ColumnCategory.FIELD);
+    categories.put("value(double)", ColumnCategory.FIELD);
+    set(ImportDataTable.class, "dataTypes", types);
+    set(ImportDataTable.class, "columnCategory", categories);
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mock(ITableSession.class);
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    doAnswer(
+            invocation -> {
+              Tablet tablet = invocation.getArgument(0);
+              assertEquals(2, tablet.getSchemas().size());
+              assertEquals("value(double)", 
tablet.getSchemas().get(1).getMeasurementName());
+              assertEquals(1, tablet.getRowSize());
+              assertEquals(1L, tablet.getTimestamps()[0]);
+              assertEquals(1.5, ((double[]) tablet.getValues()[1])[0], 0);
+              assertEquals(0, ((double[]) tablet.getValues()[0])[0], 0);
+              return null;
+            })
+        .when(session)
+        .insert(any(Tablet.class));
+    Path csv = directory.resolve("parentheses.csv");
+    Files.writeString(csv, "TIME,VALUE(DOUBLE)\n1,1.5\n");
+    try (CSVParser records = readCsvFile(csv.toString())) {
+      new ImportDataTable()
+          .writeData(
+              records.getHeaderNames(),
+              records.getRecords(),
+              directory.resolve("parentheses.failed").toString());
+    }
+    verify(session).insert(any(Tablet.class));
+    assertFalse(Files.exists(directory.resolve("parentheses.failed")));
+  }
+
+  @Test
+  public void testTypedTableCsvRejectsAmbiguousTwoColumnRoundTrip() throws 
Exception {
+    assertTableCsvTwoColumnRoundTrip(true);
+  }
+
+  @Test
+  public void testUntypedTableCsvPreservesTwoColumnRoundTrip() throws 
Exception {
+    assertTableCsvTwoColumnRoundTrip(false);
+  }
+
+  private void assertTableCsvTwoColumnRoundTrip(boolean typed) throws 
Exception {
+    Map<String, TSDataType> types = new LinkedHashMap<>();
+    types.put("value", TSDataType.DOUBLE);
+    types.put("value(double)", TSDataType.DOUBLE);
+    Map<String, ColumnCategory> categories = new LinkedHashMap<>();
+    categories.put("value", ColumnCategory.FIELD);
+    categories.put("value(double)", ColumnCategory.FIELD);
+    set(ImportDataTable.class, "dataTypes", types);
+    set(ImportDataTable.class, "columnCategory", categories);
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mockTable("t");
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    doAnswer(
+            invocation -> {
+              Tablet tablet = invocation.getArgument(0);
+              assertEquals(2, tablet.getSchemas().size());
+              assertEquals("value", 
tablet.getSchemas().get(0).getMeasurementName());
+              assertEquals("value(double)", 
tablet.getSchemas().get(1).getMeasurementName());
+              assertEquals(1, tablet.getRowSize());
+              assertEquals(1L, tablet.getTimestamps()[0]);
+              assertEquals(1.5, ((double[]) tablet.getValues()[0])[0], 0);
+              assertEquals(2.5, ((double[]) tablet.getValues()[1])[0], 0);
+              return null;
+            })
+        .when(session)
+        .insert(any(Tablet.class));
+    exportType = "csv";
+    needDataTypePrinted = typed;
+    for (int i = 0; i < 2; i++) {
+      List<String> columns =
+          i == 0
+              ? List.of("time", "value", "value(double)")
+              : List.of("time", "value(double)", "value");
+      Object[] row = i == 0 ? new Object[] {1L, 1.5, 2.5} : new Object[] {1L, 
2.5, 1.5};
+      SessionDataSet result = dataSet(columns, List.of("TIMESTAMP", "DOUBLE", 
"DOUBLE"), row);
+      when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+      assertTrue(new ExportDataTable().exportBySql("select * from t", i));
+      long failedFiles = loadFileFailedNum.sum();
+      long successfulFiles = loadFileSuccessfulNum.sum();
+      ImportDataScanTool.putToQueue(directory.resolve("dump" + i + 
"_0.csv").toString());
+      assertEquals(!typed, AbstractImportData.init(new ImportDataTable()));
+      assertEquals(failedFiles + (typed ? 1 : 0), loadFileFailedNum.sum());
+      assertEquals(successfulFiles + (typed ? 0 : 1), 
loadFileSuccessfulNum.sum());
+      assertNull(ImportDataScanTool.pollFromQueue());
+    }
+    verify(session, times(typed ? 0 : 2)).insert(any(Tablet.class));
+  }
+
+  private void assertTableCsvRoundTrip(String column) throws Exception {
+    set(ImportDataTable.class, "dataTypes", Map.of(column, TSDataType.DOUBLE));
+    set(ImportDataTable.class, "columnCategory", Map.of(column, 
ColumnCategory.FIELD));
+    ITableSessionPool pool = mock(ITableSessionPool.class);
+    ITableSession session = mockTable("t");
+    when(pool.getSession()).thenReturn(session);
+    set(ImportDataTable.class, "sessionPool", pool);
+    doAnswer(
+            invocation -> {
+              Tablet tablet = invocation.getArgument(0);
+              assertEquals(1, tablet.getSchemas().size());
+              assertEquals(column, 
tablet.getSchemas().get(0).getMeasurementName());
+              assertEquals(1, tablet.getRowSize());
+              assertEquals(1L, tablet.getTimestamps()[0]);
+              assertEquals(1.5, ((double[]) tablet.getValues()[0])[0], 0);
+              return null;
+            })
+        .when(session)
+        .insert(any(Tablet.class));
+    exportType = "csv";
+    for (boolean typed : List.of(false, true)) {
+      needDataTypePrinted = typed;
+      SessionDataSet result =
+          dataSet(List.of("time", column), List.of("TIMESTAMP", "DOUBLE"), new 
Object[] {1L, 1.5});
+      when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(result);
+      int index = typed ? 1 : 0;
+      assertTrue(new ExportDataTable().exportBySql("select * from t", index));
+      String csv = "dump" + index + "_0.csv";
+      assertEquals("time," + column + (typed ? "(DOUBLE)" : ""), 
firstLine(csv));
+      try (CSVParser records = readCsvFile(directory.resolve(csv).toString())) 
{
+        new ImportDataTable()
+            .writeData(
+                records.getHeaderNames(),
+                records.getRecords(),
+                directory.resolve("roundtrip.failed").toString());
+      }
+    }
+    verify(session, times(2)).insert(any(Tablet.class));
+    assertFalse(Files.exists(directory.resolve("roundtrip.failed")));
+  }
+
+  @Test
+  public void testSuccessArchivalPreservesFilesAndSidecars() throws Exception {
+    for (ImportTsFileOperation operation :
+        List.of(
+            ImportTsFileOperation.CP, ImportTsFileOperation.HARDLINK, 
ImportTsFileOperation.MV)) {
+      Path input = 
Files.createDirectories(directory.resolve(operation.name()).resolve("input"));
+      Path nested = Files.createDirectories(input.resolve("nested"));
+      Path source = nested.resolve("one.tsfile");
+      Path archive =
+          
Files.createDirectories(directory.resolve(operation.name()).resolve("success"));
+      for (String suffix : List.of("", ".resource", ".mods")) {
+        Files.writeString(Path.of(source + suffix), "content" + suffix);
+      }
+      successDir = archive.toString();
+      successOperation = operation;
+      ImportTsFileScanTool.setSourceFullPath(input.toString());
+      new ImportDataTree().processingFile(source.toString(), true);
+      assertTrue(Files.isDirectory(archive));
+      for (String suffix : List.of("", ".resource", ".mods")) {
+        assertEquals(
+            "content" + suffix, 
Files.readString(archive.resolve("nested/one.tsfile" + suffix)));
+        assertEquals(operation != ImportTsFileOperation.MV, 
Files.exists(Path.of(source + suffix)));
+      }
+      Path other = input.resolve("other.tsfile");
+      Files.writeString(other, "other");
+      new ImportDataTree().processingFile(other.toString(), true);
+      assertEquals("other", Files.readString(archive.resolve("other.tsfile")));
+      assertEquals("content", 
Files.readString(archive.resolve("nested/one.tsfile")));
+    }
+  }
+
+  @Test
+  public void testArchivalPreservesCollidingFlattenedNames() throws Exception {
+    for (boolean isSuccess : List.of(false, true)) {
+      for (ImportTsFileOperation operation :
+          List.of(
+              ImportTsFileOperation.CP, ImportTsFileOperation.HARDLINK, 
ImportTsFileOperation.MV)) {
+        Path root = Files.createDirectories(directory.resolve(isSuccess + "-" 
+ operation));
+        Path input = Files.createDirectories(root.resolve("input"));
+        Path archive = root.resolve("archive");
+        Files.createDirectories(input.resolve("a"));
+        successDir = failDir = archive.toString();
+        successOperation = failOperation = operation;
+        ImportTsFileScanTool.setSourceFullPath(input.toString());
+        for (String name : List.of("a/b.tsfile", "a_b.tsfile")) {
+          Path source = input.resolve(name);
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            Files.writeString(Path.of(source + suffix), name + suffix);
+          }
+          new ImportDataTree().processingFile(source.toString(), isSuccess);
+        }
+        for (String name : List.of("a/b.tsfile", "a_b.tsfile")) {
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            assertEquals(name + suffix, Files.readString(archive.resolve(name 
+ suffix)));
+            assertEquals(
+                operation != ImportTsFileOperation.MV, 
Files.exists(input.resolve(name + suffix)));
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testExistingArchiveFilesAreNotOverwritten() throws Exception {
+    for (ImportTsFileOperation operation :
+        List.of(
+            ImportTsFileOperation.CP, ImportTsFileOperation.HARDLINK, 
ImportTsFileOperation.MV)) {
+      for (String conflict : List.of("", ".resource", ".mods")) {
+        Path root = Files.createDirectories(directory.resolve(operation + "-" 
+ conflict));
+        Path input = Files.createDirectories(root.resolve("input"));
+        Path archive = Files.createDirectories(root.resolve("archive"));
+        Path source = input.resolve("one.tsfile");
+        for (String suffix : List.of("", ".resource", ".mods")) {
+          Files.writeString(Path.of(source + suffix), "source" + suffix);
+        }
+        Path occupied = archive.resolve("one.tsfile" + conflict);
+        Files.writeString(occupied, "existing");
+        successDir = archive.toString();
+        successOperation = operation;
+        ImportTsFileScanTool.setSourceFullPath(input.toString());
+        try {
+          new ImportDataTree().processingFile(source.toString(), true);
+          fail("Expected existing archive file to be rejected");
+        } catch (FileAlreadyExistsException expected) {
+          assertEquals("existing", Files.readString(occupied));
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            assertEquals("source" + suffix, Files.readString(Path.of(source + 
suffix)));
+            if (!suffix.equals(conflict)) {
+              assertFalse(Files.exists(archive.resolve("one.tsfile" + 
suffix)));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testArchivalErrorsArePropagated() throws Exception {
+    Path source = directory.resolve("one.tsfile");
+    Files.writeString(source, "source");
+    Path archive = Files.createDirectories(directory.resolve("success"));
+    Files.createDirectories(archive.resolve("one.tsfile"));
+    Files.writeString(archive.resolve("one.tsfile/child"), "occupied");
+    successDir = archive.toString();
+    successOperation = ImportTsFileOperation.CP;
+    ImportTsFileScanTool.setSourceFullPath(directory.toString());
+    try {
+      new ImportDataTree().processingFile(source.toString(), true);
+      fail("Expected archival error to propagate");
+    } catch (IOException expected) {
+      assertEquals("source", Files.readString(source));
+    }
+  }
+
+  @Test
+  public void testImportTsFileArchivalPreservesRelativePathsAndSidecars() 
throws Exception {
+    for (boolean isSuccess : List.of(false, true)) {
+      for (ImportTsFileOperation operation :
+          List.of(
+              ImportTsFileOperation.CP, ImportTsFileOperation.HARDLINK, 
ImportTsFileOperation.MV)) {
+        Path root = Files.createDirectories(directory.resolve("tsfile-" + 
isSuccess + operation));
+        Path input = Files.createDirectories(root.resolve("input"));
+        Files.createDirectories(input.resolve("a"));
+        Path archive = Files.createDirectories(root.resolve("archive"));
+        ImportTsFileBase.setSuccessAndFailDirAndOperation(
+            archive.resolve("success").toString(),
+            operation,
+            archive.resolve("fail").toString(),
+            operation);
+        ImportTsFileScanTool.setSourceFullPath(input.toString());
+
+        for (String name : List.of("a/b.tsfile", "a_b.tsfile")) {
+          Path source = input.resolve(name);
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            Files.writeString(Path.of(source + suffix), name + suffix);
+          }
+          ImportTsFileBase.processingFile(source.toString(), isSuccess);
+        }
+
+        Path destination = archive.resolve(isSuccess ? "success" : "fail");
+        for (String name : List.of("a/b.tsfile", "a_b.tsfile")) {
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            assertEquals(name + suffix, 
Files.readString(destination.resolve(name + suffix)));
+            assertEquals(
+                operation != ImportTsFileOperation.MV, 
Files.exists(input.resolve(name + suffix)));
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testImportTsFileArchiveConflictsKeepSourceFiles() throws 
Exception {
+    for (boolean isSuccess : List.of(false, true)) {
+      for (String conflict : List.of("", ".resource", ".mods")) {
+        Path root = Files.createDirectories(directory.resolve("conflict-" + 
isSuccess + conflict));
+        Path input = Files.createDirectories(root.resolve("input"));
+        Path archive = Files.createDirectories(root.resolve("archive"));
+        Path source = input.resolve("one.tsfile");
+        for (String suffix : List.of("", ".resource", ".mods")) {
+          Files.writeString(Path.of(source + suffix), "source" + suffix);
+        }
+        Path occupied = archive.resolve("one.tsfile" + conflict);
+        Files.writeString(occupied, "existing");
+        ImportTsFileScanTool.setSourceFullPath(input.toString());
+        ImportTsFileBase.setSuccessAndFailDirAndOperation(
+            archive.toString(),
+            ImportTsFileOperation.MV,
+            archive.toString(),
+            ImportTsFileOperation.MV);
+        try {
+          ImportTsFileBase.processingFile(source.toString(), isSuccess);
+          fail("Expected existing archive file to be rejected");
+        } catch (FileAlreadyExistsException expected) {
+          assertEquals("existing", Files.readString(occupied));
+          for (String suffix : List.of("", ".resource", ".mods")) {
+            assertEquals("source" + suffix, Files.readString(Path.of(source + 
suffix)));
+            if (!suffix.equals(conflict)) {
+              assertFalse(Files.exists(archive.resolve("one.tsfile" + 
suffix)));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testSingleFileRelativeSourceArchival() throws Exception {
+    Path workingDirectory = Path.of("").toAbsolutePath();
+    Path source = Files.createTempFile(workingDirectory, "one", ".tsfile");
+    try {
+      Files.writeString(source, "source");
+      Path archive = Files.createDirectories(directory.resolve("success"));
+      successDir = archive.toString();
+      successOperation = ImportTsFileOperation.CP;
+      ImportTsFileScanTool.setSourceFullPath(source.getFileName().toString());
+      new ImportDataTree().processingFile(source.toString(), true);
+      assertEquals("source", 
Files.readString(archive.resolve(source.getFileName())));
+    } finally {
+      Files.deleteIfExists(source);
+    }
+  }
+
+  private String read(String name) throws IOException {
+    return Files.readString(directory.resolve(name));
+  }
+
+  private String firstLine(String name) throws IOException {
+    return Files.readAllLines(directory.resolve(name)).get(0);
+  }
+
+  private static void set(Class<?> owner, String name, Object value) throws 
Exception {
+    Field field = owner.getDeclaredField(name);
+    field.setAccessible(true);
+    field.set(null, value);
+  }
+
+  private static Object invoke(
+      Class<?> owner, String name, Object target, Class<?>[] parameterTypes, 
Object... arguments)
+      throws Exception {
+    Method method = owner.getDeclaredMethod(name, parameterTypes);
+    method.setAccessible(true);
+    try {
+      return method.invoke(target, arguments);
+    } catch (InvocationTargetException e) {
+      throw (Exception) e.getCause();
+    }
+  }
+
+  private static void mockTree(SessionDataSet dataSet) throws Exception {
+    Session session = mock(Session.class);
+    when(session.executeQueryStatement(anyString(), 
anyLong())).thenReturn(dataSet);
+    set(ExportDataTree.class, "session", session);
+  }
+
+  private static ITableSession mockTable(String... names) throws Exception {
+    ITableSession session = mock(ITableSession.class);
+    set(ExportDataTable.class, "tableSession", session);
+    set(ExportDataTable.class, "tables", new 
ArrayList<>(Arrays.asList(names)));
+    set(ExportDataTable.class, "allTables", new 
ArrayList<>(Arrays.asList(names)));
+    return session;
+  }
+
+  private static SessionDataSet dataSet(List<String> names, List<String> 
types, Object[]... rows)
+      throws Exception {
+    SessionDataSet dataSet = mock(SessionDataSet.class);
+    SessionDataSet.DataIterator iterator = 
mock(SessionDataSet.DataIterator.class);
+    AtomicInteger row = new AtomicInteger(-1);
+    when(dataSet.getColumnNames()).thenAnswer(invocation -> new 
ArrayList<>(names));
+    when(dataSet.getColumnTypes()).thenReturn(types);
+    when(dataSet.iterator()).thenReturn(iterator);
+    when(iterator.getColumnTypeList()).thenReturn(types);
+    when(iterator.next()).thenAnswer(invocation -> row.incrementAndGet() < 
rows.length);
+    when(iterator.getString(anyInt()))
+        .thenAnswer(
+            invocation -> {
+              Object value = rows[row.get()][(Integer) 
invocation.getArgument(0) - 1];
+              return value == null ? null : value.toString();
+            });
+    when(iterator.getLong(anyInt()))
+        .thenAnswer(
+            invocation ->
+                ((Number) rows[row.get()][(Integer) invocation.getArgument(0) 
- 1]).longValue());
+    when(iterator.isNull(anyInt()))
+        .thenAnswer(invocation -> rows[row.get()][(Integer) 
invocation.getArgument(0) - 1] == null);
+    return dataSet;
+  }
+}
diff --git 
a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/tsfile/ImportTsFileTest.java
 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/tsfile/ImportTsFileTest.java
new file mode 100644
index 00000000000..481f678b52e
--- /dev/null
+++ 
b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/tsfile/ImportTsFileTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.tool.tsfile;
+
+import org.apache.iotdb.rpc.StatementExecutionException;
+import org.apache.iotdb.session.pool.SessionPool;
+import org.apache.iotdb.tool.common.ImportTsFileOperation;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+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.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class ImportTsFileTest extends AbstractTsFileTool {
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private final Map<Field, Object> originalFields = new HashMap<>();
+  private SessionPool sessionPool;
+  private Path input;
+  private Path archive;
+
+  @Before
+  public void setUp() throws Exception {
+    for (Class<?> owner :
+        List.of(
+            ImportTsFile.class,
+            ImportTsFileBase.class,
+            ImportTsFileLocally.class,
+            ImportTsFileRemotely.class,
+            ImportTsFileScanTool.class)) {
+      for (Field field : owner.getDeclaredFields()) {
+        if (Modifier.isStatic(field.getModifiers()) && 
!Modifier.isFinal(field.getModifiers())) {
+          field.setAccessible(true);
+          originalFields.put(field, field.get(null));
+        }
+      }
+    }
+    sessionPool = mock(SessionPool.class);
+    ImportTsFileLocally.setSessionPool(sessionPool);
+    setImportOption("isRemoteLoad", false);
+    setImportOption("threadNum", 2);
+    input = 
Files.createDirectories(temporaryFolder.getRoot().toPath().resolve("input"));
+    archive = 
Files.createDirectories(temporaryFolder.getRoot().toPath().resolve("archive"));
+    ImportTsFileScanTool.setSourceFullPath(input.toString());
+    ImportTsFileBase.setSuccessAndFailDirAndOperation(
+        archive.toString(), ImportTsFileOperation.CP, archive.toString(), 
ImportTsFileOperation.CP);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    while (ImportTsFileScanTool.pollFromQueue() != null) {
+      // Drain any files left by a failing assertion before restoring the 
shared state.
+    }
+    ImportTsFileBase.resetCounters();
+    for (Map.Entry<Field, Object> entry : originalFields.entrySet()) {
+      entry.getKey().set(null, entry.getValue());
+    }
+  }
+
+  @Test
+  public void testSuccessfulImportReturnsZero() throws Exception {
+    Path source = createTsFileGroup("one.tsfile");
+    ImportTsFileScanTool.putToQueue(source.toString());
+    assertEquals(CODE_OK, ImportTsFile.asyncImportTsFiles());
+    assertArchived("one.tsfile");
+    verify(sessionPool).executeNonQueryStatement(anyString());
+    assertNull(ImportTsFileScanTool.pollFromQueue());
+  }
+
+  @Test
+  public void testArchiveConflictsReturnErrorAndContinueImport() throws 
Exception {
+    for (ImportTsFileOperation operation :
+        List.of(
+            ImportTsFileOperation.CP, ImportTsFileOperation.HARDLINK, 
ImportTsFileOperation.MV)) {
+      ImportTsFileBase.setSuccessAndFailDirAndOperation(
+          archive.toString(), operation, archive.toString(), operation);
+      for (String conflict : List.of("", ".resource", ".mods")) {
+        String name = operation + conflict + ".tsfile";
+        Path source = createTsFileGroup(name);
+        Path occupied = archive.resolve(name + conflict);
+        Files.writeString(occupied, "existing");
+        String goodName = "good-" + name;
+        Path good = createTsFileGroup(goodName);
+        ImportTsFileScanTool.putToQueue(source.toString());
+        ImportTsFileScanTool.putToQueue(good.toString());
+
+        assertEquals(CODE_ERROR, ImportTsFile.asyncImportTsFiles());
+        assertEquals("existing", Files.readString(occupied));
+        for (String suffix : List.of("", ".resource", ".mods")) {
+          assertEquals("source" + suffix, Files.readString(Path.of(source + 
suffix)));
+          if (!suffix.equals(conflict)) {
+            assertFalse(Files.exists(archive.resolve(name + suffix)));
+          }
+        }
+        assertArchived(goodName);
+        assertNull(ImportTsFileScanTool.pollFromQueue());
+      }
+    }
+    verify(sessionPool, times(18)).executeNonQueryStatement(anyString());
+  }
+
+  @Test
+  public void testArchiveIoFailureReturnsError() throws Exception {
+    Files.createDirectories(input.resolve("nested"));
+    Path source = createTsFileGroup("nested/one.tsfile");
+    Files.writeString(archive.resolve("nested"), "blocking file");
+    ImportTsFileScanTool.putToQueue(source.toString());
+    assertEquals(CODE_ERROR, ImportTsFile.asyncImportTsFiles());
+    assertEquals("source", Files.readString(source));
+    assertEquals("blocking file", Files.readString(archive.resolve("nested")));
+  }
+
+  @Test
+  public void testLoadFailureReturnsErrorWhenArchiveSucceeds() throws 
Exception {
+    Path source = createTsFileGroup("one.tsfile");
+    doThrow(new StatementExecutionException("load failed"))
+        .when(sessionPool)
+        .executeNonQueryStatement(anyString());
+    ImportTsFileScanTool.putToQueue(source.toString());
+    assertEquals(CODE_ERROR, ImportTsFile.asyncImportTsFiles());
+    assertArchived("one.tsfile");
+    verify(sessionPool).executeNonQueryStatement(anyString());
+  }
+
+  @Test
+  public void testExitCodeResetsForNextImport() throws Exception {
+    Path source = createTsFileGroup("one.tsfile");
+    Path occupied = archive.resolve("one.tsfile");
+    Files.writeString(occupied, "existing");
+    ImportTsFileScanTool.putToQueue(source.toString());
+    assertEquals(CODE_ERROR, ImportTsFile.asyncImportTsFiles());
+    Files.delete(occupied);
+    ImportTsFileScanTool.putToQueue(source.toString());
+    assertEquals(CODE_OK, ImportTsFile.asyncImportTsFiles());
+    assertArchived("one.tsfile");
+    verify(sessionPool, times(2)).executeNonQueryStatement(anyString());
+  }
+
+  @Test
+  public void testImportFromTargetPathReturnsErrorForUnprocessedFiles() throws 
Exception {
+    setImportOption("threadNum", 0);
+    // Exercise the entry point without starting workers or connecting to a 
server.
+    setImportOption("isRemoteLoad", true);
+    Path source = createTsFileGroup("one.tsfile");
+    setImportOption("source", source.toString());
+    assertEquals(CODE_ERROR, ImportTsFile.importFromTargetPath());
+    assertEquals(source.toString(), ImportTsFileScanTool.pollFromQueue());
+  }
+
+  private Path createTsFileGroup(String name) throws Exception {
+    Path source = input.resolve(name);
+    for (String suffix : List.of("", ".resource", ".mods")) {
+      Files.writeString(Path.of(source + suffix), "source" + suffix);
+    }
+    return source;
+  }
+
+  private void assertArchived(String name) throws Exception {
+    for (String suffix : List.of("", ".resource", ".mods")) {
+      assertEquals("source" + suffix, Files.readString(archive.resolve(name + 
suffix)));
+    }
+  }
+
+  private static void setImportOption(String name, Object value) throws 
Exception {
+    Field field = ImportTsFile.class.getDeclaredField(name);
+    field.setAccessible(true);
+    field.set(null, value);
+  }
+}
diff --git a/scripts/tools/windows/export-data.bat 
b/scripts/tools/windows/export-data.bat
index f239b09bb48..06d489a2d30 100644
--- a/scripts/tools/windows/export-data.bat
+++ b/scripts/tools/windows/export-data.bat
@@ -57,6 +57,4 @@ pause
 @REM 
-----------------------------------------------------------------------------
 :finally
 
-ENDLOCAL
-
-EXIT /B %ret_code%
+ENDLOCAL & EXIT /B %ret_code%
diff --git a/scripts/tools/windows/export-tsfile.bat 
b/scripts/tools/windows/export-tsfile.bat
index 0b070b51fbc..95c9e5c29e9 100644
--- a/scripts/tools/windows/export-tsfile.bat
+++ b/scripts/tools/windows/export-tsfile.bat
@@ -57,6 +57,4 @@ pause
 @REM 
-----------------------------------------------------------------------------
 :finally
 
-ENDLOCAL
-
-EXIT /B %ret_code%
\ No newline at end of file
+ENDLOCAL & EXIT /B %ret_code%
diff --git a/scripts/tools/windows/import-data.bat 
b/scripts/tools/windows/import-data.bat
index 56518c78600..89d2feee0f6 100644
--- a/scripts/tools/windows/import-data.bat
+++ b/scripts/tools/windows/import-data.bat
@@ -58,6 +58,4 @@ pause
 @REM 
-----------------------------------------------------------------------------
 :finally
 
-ENDLOCAL
-
-EXIT /B %ret_code%
+ENDLOCAL & EXIT /B %ret_code%
diff --git a/scripts/tools/windows/schema/export-schema.bat 
b/scripts/tools/windows/schema/export-schema.bat
index 9a082ac05ff..c7f29781433 100644
--- a/scripts/tools/windows/schema/export-schema.bat
+++ b/scripts/tools/windows/schema/export-schema.bat
@@ -57,6 +57,4 @@ pause
 @REM 
-----------------------------------------------------------------------------
 :finally
 
-ENDLOCAL
-
-EXIT /B %ret_code%
+ENDLOCAL & EXIT /B %ret_code%
diff --git a/scripts/tools/windows/schema/import-schema.bat 
b/scripts/tools/windows/schema/import-schema.bat
index a4ddd93b6ff..930da21d1cc 100644
--- a/scripts/tools/windows/schema/import-schema.bat
+++ b/scripts/tools/windows/schema/import-schema.bat
@@ -58,6 +58,4 @@ pause
 @REM 
-----------------------------------------------------------------------------
 :finally
 
-ENDLOCAL
-
-EXIT /B %ret_code%
+ENDLOCAL & EXIT /B %ret_code%

Reply via email to