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

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


The following commit(s) were added to refs/heads/master by this push:
     new 513a093f6dc Fix silent deduplication of duplicate tag columns in COPY 
TO statement (#18408)
513a093f6dc is described below

commit 513a093f6dc4f691c900c4a28f8afafaedb7b115
Author: shuwenwei <[email protected]>
AuthorDate: Thu Aug 6 14:49:16 2026 +0800

    Fix silent deduplication of duplicate tag columns in COPY TO statement 
(#18408)
---
 .../query/recent/copyto/IoTDBCopyToTsFileIT.java   | 38 ++++++++++++++++++++--
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  4 +++
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  4 +++
 .../process/copyto/tsfile/CopyToTsFileOptions.java | 13 ++++++--
 .../copyto/tsfile/TsFileFormatCopyToWriter.java    |  6 +++-
 .../plan/relational/sql/parser/AstBuilder.java     | 31 +++++++++++++++++-
 .../schema/column/ColumnHeaderConstant.java        |  2 +-
 7 files changed, 90 insertions(+), 8 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
index 04bed8b91a1..e25c3f58299 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/copyto/IoTDBCopyToTsFileIT.java
@@ -276,6 +276,38 @@ public class IoTDBCopyToTsFileIT {
     }
   }
 
+  @Test
+  public void testDuplicateTagColumns()
+      throws IoTDBConnectionException, StatementExecutionException, 
IOException {
+    try (ITableSession session =
+        EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE_NAME)) {
+      try {
+        session.executeQueryStatement(
+            "copy table1(time,tag1,tag2,s1) to 'dup.tsfile' with 
(tags(tag1,tag1), memory_threshold 1000000)");
+        Assert.fail("Should report duplicate tag column error");
+      } catch (StatementExecutionException e) {
+        Assert.assertTrue(
+            e.getMessage(), e.getMessage().contains("Duplicate tag column in 
TAGS clause: tag1"));
+      }
+    }
+  }
+
+  @Test
+  public void testDuplicateOption()
+      throws IoTDBConnectionException, StatementExecutionException, 
IOException {
+    try (ITableSession session =
+        EnvFactory.getEnv().getTableSessionConnectionWithDB(DATABASE_NAME)) {
+      try {
+        session.executeQueryStatement(
+            "copy table1(time,tag1,tag2,s1) to 'dup_option.tsfile' with 
(tags(tag1), tags(tag2), memory_threshold 1000000)");
+        Assert.fail("Should report duplicate option error");
+      } catch (StatementExecutionException e) {
+        Assert.assertTrue(
+            e.getMessage(), e.getMessage().contains("Duplicate option in COPY 
TO statement: TAGS"));
+      }
+    }
+  }
+
   @Test
   public void testCopyWithSpecifiedTag()
       throws IoTDBConnectionException, StatementExecutionException, 
IOException {
@@ -474,7 +506,7 @@ public class IoTDBCopyToTsFileIT {
         Assert.assertEquals(2, rowCount);
         Assert.assertEquals(1, deviceCount);
         Assert.assertTrue(sizeInBytes > 0);
-        Assert.assertEquals("default", tableName);
+        Assert.assertEquals("default(auto_gen)", tableName);
         Assert.assertEquals("time(auto_gen)", timeColumn);
         Assert.assertEquals("[]", tagColumns);
 
@@ -515,7 +547,7 @@ public class IoTDBCopyToTsFileIT {
         Assert.assertEquals(2, rowCount);
         Assert.assertEquals(1, deviceCount);
         Assert.assertTrue(sizeInBytes > 0);
-        Assert.assertEquals("default", tableName);
+        Assert.assertEquals("default(auto_gen)", tableName);
         Assert.assertEquals("time(auto_gen)", timeColumn);
         Assert.assertEquals("[]", tagColumns);
 
@@ -648,7 +680,7 @@ public class IoTDBCopyToTsFileIT {
         Assert.assertEquals(1, rowCount);
         Assert.assertEquals(1, deviceCount);
         Assert.assertTrue(sizeInBytes > 0);
-        Assert.assertEquals("default", tableName);
+        Assert.assertEquals("default(auto_gen)", tableName);
         Assert.assertEquals("time", timeColumn);
         Assert.assertEquals("[]", tagColumns);
 
diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 40e7ed020c3..0e9e8158ac7 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -2424,6 +2424,10 @@ public final class DataNodeQueryMessages {
       "Note that the syntax for 'set configuration' in the tree model is not 
exactly the same as that in the table model.";
   public static final String UNSUPPORTED_COPY_TO_FORMAT_S_SUPPORTED_FORMATS_S =
       "Unsupported COPY TO format '%s'. Supported formats: %s";
+  public static final String 
EXCEPTION_DUPLICATE_TAG_COLUMN_IN_TAGS_CLAUSE_ARG_61FD5422 =
+      "Duplicate tag column in TAGS clause: %s";
+  public static final String 
EXCEPTION_DUPLICATE_OPTION_IN_COPY_TO_STATEMENT_ARG_99CFE09F =
+      "Duplicate option in COPY TO statement: %s";
   public static final String 
SIMULTANEOUS_SETTING_OF_MONTHLY_AND_NON_MONTHLY_INTERVALS_IS_NOT_SUPPORTED =
       "Simultaneous setting of monthly and non-monthly intervals is not 
supported.";
   public static final String 
DON_T_NEED_TO_SPECIFY_TIME_COLUMN_WHILE_EITHER_TIME_BOUND_OR_FILL_GROUP_PARAMETER_IS_NOT
 =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index f236a57f6a4..1ab922fa121 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -2799,6 +2799,10 @@ public final class DataNodeQueryMessages {
   public static final String UNSUPPORTED_COPY_TO_FORMAT_S_SUPPORTED_FORMATS_S =
 
       "不支持 COPY_TO 格式 '%s'。支持的格式:%s";
+  public static final String 
EXCEPTION_DUPLICATE_TAG_COLUMN_IN_TAGS_CLAUSE_ARG_61FD5422 =
+      "TAGS 子句中存在重复的 TAG 列:%s";
+  public static final String 
EXCEPTION_DUPLICATE_OPTION_IN_COPY_TO_STATEMENT_ARG_99CFE09F =
+      "COPY TO 语句中存在重复的选项:%s";
   public static final String 
SIMULTANEOUS_SETTING_OF_MONTHLY_AND_NON_MONTHLY_INTERVALS_IS_NOT_SUPPORTED =
       "不支持同时设置月级和非月级时间间隔。";
   public static final String 
DON_T_NEED_TO_SPECIFY_TIME_COLUMN_WHILE_EITHER_TIME_BOUND_OR_FILL_GROUP_PARAMETER_IS_NOT
 =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
index 17f2b689356..22b2e25a6df 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/CopyToTsFileOptions.java
@@ -62,6 +62,7 @@ public class CopyToTsFileOptions implements CopyToOptions {
   private final long targetMemoryThreshold;
 
   private boolean generateNewTimeColumn = false;
+  private boolean generateNewTableName = false;
 
   public CopyToTsFileOptions(
       String targetTableName,
@@ -82,6 +83,10 @@ public class CopyToTsFileOptions implements CopyToOptions {
     return generateNewTimeColumn;
   }
 
+  public boolean isGenerateNewTableName() {
+    return generateNewTableName;
+  }
+
   @Override
   public void infer(
       Analysis analysis, RelationPlan queryRelationPlan, List<ColumnHeader> 
columnHeaders) {
@@ -91,8 +96,12 @@ public class CopyToTsFileOptions implements CopyToOptions {
       onlyOneQueriedTable = tables.iterator().next();
     }
     if (targetTableName == null) {
-      targetTableName =
-          onlyOneQueriedTable == null ? DEFAULT_TABLE_NAME : 
onlyOneQueriedTable.getTableName();
+      if (onlyOneQueriedTable == null) {
+        targetTableName = DEFAULT_TABLE_NAME;
+        generateNewTableName = true;
+      } else {
+        targetTableName = onlyOneQueriedTable.getTableName();
+      }
     }
     if (onlyOneQueriedTable != null) {
       inferTimeAndTagsWithTable(onlyOneQueriedTable, columnHeaders);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/TsFileFormatCopyToWriter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/TsFileFormatCopyToWriter.java
index 65a8b239db7..08b8ce127ba 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/TsFileFormatCopyToWriter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/process/copyto/tsfile/TsFileFormatCopyToWriter.java
@@ -52,6 +52,7 @@ public class TsFileFormatCopyToWriter implements 
IFormatCopyToWriter {
   private final String targetTimeColumn;
   private final Set<String> targetTagColumns;
   private final boolean generateNewTimeColumn;
+  private final boolean generateNewTableName;
 
   private MemoizedCheckedSupplier<TableTsBlock2TsFileWriter, IOException> 
tsFileWriter;
   private long rowCount = 0;
@@ -66,6 +67,7 @@ public class TsFileFormatCopyToWriter implements 
IFormatCopyToWriter {
     this.targetTableName = copyToOptions.getTargetTableName();
     this.targetTimeColumn = copyToOptions.getTargetTimeColumn();
     this.generateNewTimeColumn = copyToOptions.isGenerateNewTimeColumn();
+    this.generateNewTableName = copyToOptions.isGenerateNewTableName();
     targetTagColumns = copyToOptions.getTargetTagColumns();
 
     List<ColumnSchema> columnSchemas =
@@ -165,7 +167,9 @@ public class TsFileFormatCopyToWriter implements 
IFormatCopyToWriter {
     builder.getValueColumnBuilders()[2].writeLong(deviceCount);
     builder.getValueColumnBuilders()[3].writeLong(targetFile.length());
     builder.getValueColumnBuilders()[4].writeBinary(
-        new Binary(targetTableName, TSFileConfig.STRING_CHARSET));
+        new Binary(
+            targetTableName + (generateNewTableName ? AUTO_GEN_MARK : ""),
+            TSFileConfig.STRING_CHARSET));
     builder.getValueColumnBuilders()[5].writeBinary(
         new Binary(
             targetTimeColumn + (generateNewTimeColumn ? AUTO_GEN_MARK : ""),
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
index 7db81826ab6..87e1b417e44 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
@@ -2270,8 +2270,17 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
     String targetFileName = parseStringLiteral(ctx.fileName.getText());
     CopyToOptions.Builder copyToOptionsBuilder = new CopyToOptions.Builder();
     if (optionsContext != null) {
+      Set<String> optionKeys = new HashSet<>();
       for (RelationalSqlParser.CopyToStatementOptionContext context :
           optionsContext.copyToStatementOption()) {
+        String optionKey = getCopyToOptionKey(context);
+        if (!optionKeys.add(optionKey)) {
+          throw new SemanticException(
+              String.format(
+                  DataNodeQueryMessages
+                      
.EXCEPTION_DUPLICATE_OPTION_IN_COPY_TO_STATEMENT_ARG_99CFE09F,
+                  optionKey));
+        }
         addCopyToOption(copyToOptionsBuilder, context);
       }
     }
@@ -2296,6 +2305,20 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
     return new CopyTo(queryNode, targetFileName, copyToOptionsBuilder.build());
   }
 
+  private String 
getCopyToOptionKey(RelationalSqlParser.CopyToStatementOptionContext context) {
+    if (context.FORMAT() != null) {
+      return "FORMAT";
+    } else if (context.TABLE() != null) {
+      return "TABLE";
+    } else if (context.TIME() != null) {
+      return "TIME";
+    } else if (context.TAGS() != null) {
+      return "TAGS";
+    } else {
+      return "MEMORY_THRESHOLD";
+    }
+  }
+
   private void addCopyToOption(
       CopyToOptions.Builder builder, 
RelationalSqlParser.CopyToStatementOptionContext context) {
     if (context.FORMAT() != null) {
@@ -2325,7 +2348,13 @@ public class AstBuilder extends 
RelationalSqlBaseVisitor<Node> {
           context.identifierList().identifier();
       Set<String> targetTagColumns = new 
LinkedHashSet<>(identifierList.size());
       for (RelationalSqlParser.IdentifierContext identifierContext : 
identifierList) {
-        targetTagColumns.add(((Identifier) 
visit(identifierContext)).getValue());
+        String tagColumnName = ((Identifier) 
visit(identifierContext)).getValue();
+        if (!targetTagColumns.add(tagColumnName)) {
+          throw new SemanticException(
+              String.format(
+                  
DataNodeQueryMessages.EXCEPTION_DUPLICATE_TAG_COLUMN_IN_TAGS_CLAUSE_ARG_61FD5422,
+                  tagColumnName));
+        }
       }
       builder.withTargetTagColumns(targetTagColumns);
     } else if (context.MEMORY_THRESHOLD() != null) {
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java
index 18653a52c57..87c6a01097b 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/column/ColumnHeaderConstant.java
@@ -840,7 +840,7 @@ public class ColumnHeaderConstant {
 
   public static final List<ColumnHeader> COPY_TO_TSFILE_COLUMN_HEADERS =
       ImmutableList.of(
-          new ColumnHeader(PATH.toLowerCase(), TSDataType.TEXT),
+          new ColumnHeader(PATH.toLowerCase(), TSDataType.STRING),
           new ColumnHeader(ROW_COUNT, TSDataType.INT64),
           new ColumnHeader(DEVICE_COUNT, TSDataType.INT64),
           new ColumnHeader(SIZE_IN_BYTES_TABLE_MODEL, TSDataType.INT64),

Reply via email to