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

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


The following commit(s) were added to refs/heads/master by this push:
     new 90959ab504c Fix UPDATE attribute column validation (#18705)
90959ab504c is described below

commit 90959ab504cb4012ce6b0cd963c460e48cc5d7df
Author: Caideyipi <[email protected]>
AuthorDate: Mon Sep 28 09:56:33 2026 +0800

    Fix UPDATE attribute column validation (#18705)
    
    * Fix UPDATE attribute column validation
    
    * Clarify UPDATE column validation errors
    
    * Distinguish unknown UPDATE target columns
---
 .../iotdb/relational/it/schema/IoTDBDeviceIT.java  |  46 +++++-
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   6 +
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   6 +
 .../relational/analyzer/StatementAnalyzer.java     | 155 +++++++++++++++++----
 .../schemaregion/impl/SchemaRegionMemoryImpl.java  |  81 +++++++++--
 .../SchemaRegionSimpleRecoverTest.java             |  76 ++++++++++
 .../plan/relational/analyzer/AnalyzerTest.java     | 138 ++++++++++++++++++
 7 files changed, 464 insertions(+), 44 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDeviceIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDeviceIT.java
index 1923210b50d..1eb2da9d100 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDeviceIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBDeviceIT.java
@@ -38,7 +38,6 @@ import java.sql.Statement;
 import java.util.Collections;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(IoTDBTestRunner.class)
@@ -192,7 +191,18 @@ public class IoTDBDeviceIT {
         statement.execute("update table0 set device_id = '1'");
         fail("Update shall fail for tag");
       } catch (final Exception e) {
-        assertEquals("701: Update can only specify attribute columns.", 
e.getMessage());
+        assertEquals(
+            "701: Cannot update TAG column 'device_id'. UPDATE can only 
specify ATTRIBUTE columns.",
+            e.getMessage());
+      }
+
+      try {
+        statement.execute("update table0 set time = 1");
+        fail("Update shall fail for time column");
+      } catch (final Exception e) {
+        assertEquals(
+            "701: Cannot update TIME column 'time'. UPDATE can only specify 
ATTRIBUTE columns.",
+            e.getMessage());
       }
 
       try {
@@ -209,6 +219,25 @@ public class IoTDBDeviceIT {
         assertEquals("616: Column 'col' cannot be resolved", e.getMessage());
       }
 
+      try {
+        statement.execute("update table0 set temperature = '1'");
+        fail("Update shall fail for field column");
+      } catch (final Exception e) {
+        assertEquals(
+            "701: Cannot update FIELD column 'temperature'. UPDATE can only 
specify ATTRIBUTE columns.",
+            e.getMessage());
+      }
+
+      try {
+        statement.execute(
+            "update table0 set temperature = '1' " + "where region_id = '1' 
and region_id = '2'");
+        fail("Update shall validate field column before finishing an empty 
update");
+      } catch (final Exception e) {
+        assertEquals(
+            "701: Cannot update FIELD column 'temperature'. UPDATE can only 
specify ATTRIBUTE columns.",
+            e.getMessage());
+      }
+
       try {
         statement.execute("update table0 set model = cast(device_id as 
int32)");
         fail("Update shall fail when result type mismatch");
@@ -241,7 +270,18 @@ public class IoTDBDeviceIT {
         statement.execute("update table0 set model = humidity");
         fail("Update shall fail for non-tag/attribute columns");
       } catch (final Exception e) {
-        assertTrue(e.getMessage().contains("Column 'humidity' is not an 
attribute or tag column"));
+        assertEquals(
+            "701: Cannot reference FIELD column 'humidity' in an UPDATE value. 
UPDATE values can only reference ATTRIBUTE or TAG columns.",
+            e.getMessage());
+      }
+
+      try {
+        statement.execute("update table0 set model = time");
+        fail("Update shall fail for time column in value expression");
+      } catch (final Exception e) {
+        assertEquals(
+            "701: Cannot reference TIME column 'time' in an UPDATE value. 
UPDATE values can only reference ATTRIBUTE or TAG columns.",
+            e.getMessage());
       }
 
       // Test filter with no effect
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 b2c6f4662fb..c9e2a53506e 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
@@ -799,6 +799,12 @@ public final class DataNodeQueryMessages {
       "Show Index statement is not supported yet.";
   public static final String UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS =
       "Update can only specify attribute columns.";
+  public static final String
+      
EXCEPTION_CANNOT_UPDATE_ARG_COLUMN_ARG_UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS_F805D1A6
 =
+          "Cannot update %s column '%s'. UPDATE can only specify ATTRIBUTE 
columns.";
+  public static final String
+      
EXCEPTION_CANNOT_REFERENCE_ARG_COLUMN_ARG_IN_AN_UPDATE_VALUE_UPDATE_VALUES_CAN_ONLY_REFERENCE_ATTRIBUTE_OR_TAG_COLUMNS_C01BE71A
 =
+          "Cannot reference %s column '%s' in an UPDATE value. UPDATE values 
can only reference ATTRIBUTE or TAG columns.";
   public static final String DROP_FUNCTION_STATEMENT_IS_NOT_SUPPORTED_YET =
       "Drop Function statement is not supported yet.";
   public static final String SHOW_FUNCTION_STATEMENT_IS_NOT_SUPPORTED_YET =
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 849e726ccbb..857031856b7 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
@@ -779,6 +779,12 @@ public final class DataNodeQueryMessages {
       "暂不支持 Show Index 语句。";
   public static final String UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS =
       "UPDATE 只能指定属性列。";
+  public static final String
+      
EXCEPTION_CANNOT_UPDATE_ARG_COLUMN_ARG_UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS_F805D1A6
 =
+          "无法更新 %s 列 '%s'。UPDATE 只能指定 ATTRIBUTE 列。";
+  public static final String
+      
EXCEPTION_CANNOT_REFERENCE_ARG_COLUMN_ARG_IN_AN_UPDATE_VALUE_UPDATE_VALUES_CAN_ONLY_REFERENCE_ATTRIBUTE_OR_TAG_COLUMNS_C01BE71A
 =
+          "无法在 UPDATE 值中引用 %s 列 '%s'。UPDATE 值只能引用 ATTRIBUTE 或 TAG 列。";
   public static final String DROP_FUNCTION_STATEMENT_IS_NOT_SUPPORTED_YET =
       "暂不支持 Drop Function 语句。";
   public static final String SHOW_FUNCTION_STATEMENT_IS_NOT_SUPPORTED_YET =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
index bd985e89701..88f605f6dfd 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/StatementAnalyzer.java
@@ -169,6 +169,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CreatePipePlugin;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CreateTable;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CreateTopic;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.CreateView;
+import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DefaultExpressionTraversalVisitor;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Delete;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DeleteDevice;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DescribeTable;
@@ -971,46 +972,22 @@ public class StatementAnalyzer {
       final TsTable table =
           DataNodeTableCache.getInstance().getTable(node.getDatabase(), 
node.getTableName());
       DataNodeTreeViewSchemaUtils.checkTableInWrite(node.getDatabase(), table);
-      if (!node.parseWhere(
-          null,
-          table,
-          table.getColumnList().stream()
-              .filter(
-                  columnSchema ->
-                      
columnSchema.getColumnCategory().equals(TsTableColumnCategory.ATTRIBUTE))
-              .map(TsTableColumnSchema::getColumnName)
-              .collect(Collectors.toList()),
-          queryContext)) {
-        analysis.setFinishQueryAfterAnalyze();
-        return null;
-      }
 
       // If node.location is absent, this is a pipe-transferred update, namely 
the assignments are
-      // already parsed at the sender
+      // already parsed at the sender. Still validate the target columns at 
the receiver.
       if (node.getLocation().isPresent()) {
-        final Set<SymbolReference> attributeNames = new HashSet<>();
+        final Set<String> attributeNames = new HashSet<>();
         node.setAssignments(
             node.getAssignments().stream()
                 .map(
                     assignment -> {
+                      validateUpdateTargetColumn(assignment.getName(), table);
                       final Expression parsedColumn =
                           analyzeAndRewriteExpression(
                                   translationMap, translationMap.getScope(), 
assignment.getName())
                               .getRight();
-                      if (!(parsedColumn instanceof SymbolReference)
-                          || table
-                                  .getColumnSchema(((SymbolReference) 
parsedColumn).getName())
-                                  .getColumnCategory()
-                              != TsTableColumnCategory.ATTRIBUTE) {
-                        throw new SemanticException(
-                            
DataNodeQueryMessages.UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS);
-                      }
-                      if (attributeNames.contains(parsedColumn)) {
-                        throw new SemanticException(
-                            DataNodeQueryMessages
-                                
.UPDATE_ATTRIBUTE_SHALL_SPECIFY_A_ATTRIBUTE_ONLY_ONCE);
-                      }
-                      attributeNames.add((SymbolReference) parsedColumn);
+                      validateUpdateAttributeColumn(parsedColumn, table, 
attributeNames);
+                      validateUpdateValueExpression(assignment.getValue(), 
table);
 
                       final Pair<Type, Expression> expressionPair;
                       try {
@@ -1040,10 +1017,130 @@ public class StatementAnalyzer {
                       return new UpdateAssignment(parsedColumn, 
expressionPair.getRight());
                     })
                 .collect(Collectors.toList()));
+      } else {
+        final Set<String> attributeNames = new HashSet<>();
+        node.getAssignments()
+            .forEach(
+                assignment -> {
+                  validateUpdateAttributeColumn(assignment.getName(), table, 
attributeNames);
+                  validateUpdateValueExpression(assignment.getValue(), table);
+                });
+      }
+
+      if (!node.parseWhere(
+          null,
+          table,
+          table.getColumnList().stream()
+              .filter(
+                  columnSchema ->
+                      
columnSchema.getColumnCategory().equals(TsTableColumnCategory.ATTRIBUTE))
+              .map(TsTableColumnSchema::getColumnName)
+              .collect(Collectors.toList()),
+          queryContext)) {
+        analysis.setFinishQueryAfterAnalyze();
+        return null;
       }
       return null;
     }
 
+    private void validateUpdateAttributeColumn(
+        final Expression column, final TsTable table, final Set<String> 
attributeNames) {
+      validateUpdateTargetColumn(column, table);
+      if (!(column instanceof SymbolReference)) {
+        throw new SemanticException(
+            DataNodeQueryMessages.UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS);
+      }
+
+      final String columnName = ((SymbolReference) column).getName();
+      final TsTableColumnSchema columnSchema = 
table.getColumnSchema(columnName);
+      if (Objects.isNull(columnSchema)) {
+        throw new SemanticException(
+            String.format(DataNodeQueryMessages.COLUMN_S_CANNOT_BE_RESOLVED, 
columnName));
+      }
+      if (!attributeNames.add(columnName)) {
+        throw new SemanticException(
+            
DataNodeQueryMessages.UPDATE_ATTRIBUTE_SHALL_SPECIFY_A_ATTRIBUTE_ONLY_ONCE);
+      }
+    }
+
+    private void validateUpdateTargetColumn(final Expression column, final 
TsTable table) {
+      final String columnName;
+      if (column instanceof Identifier) {
+        columnName = ((Identifier) column).getValue().toLowerCase(ENGLISH);
+      } else if (column instanceof SymbolReference) {
+        columnName = ((SymbolReference) column).getName();
+      } else {
+        return;
+      }
+
+      final TsTableColumnSchema columnSchema = 
table.getColumnSchema(columnName);
+      if (Objects.nonNull(columnSchema)
+          && columnSchema.getColumnCategory() != 
TsTableColumnCategory.ATTRIBUTE) {
+        throw new SemanticException(
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_CANNOT_UPDATE_ARG_COLUMN_ARG_UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS_F805D1A6,
+                columnSchema.getColumnCategory(),
+                columnName));
+      }
+    }
+
+    private void validateUpdateValueExpression(final Expression expression, 
final TsTable table) {
+      final Set<String> unknownColumnNames = new HashSet<>();
+      final Map<String, TsTableColumnCategory> invalidColumns = new 
LinkedHashMap<>();
+      new DefaultExpressionTraversalVisitor<Void>() {
+        @Override
+        public Void visitIdentifier(final Identifier node, final Void context) 
{
+          collectUpdateValueColumn(
+              node.getValue().toLowerCase(ENGLISH), table, unknownColumnNames, 
invalidColumns);
+          return null;
+        }
+
+        @Override
+        public Void visitSymbolReference(final SymbolReference node, final 
Void context) {
+          collectUpdateValueColumn(node.getName(), table, unknownColumnNames, 
invalidColumns);
+          return null;
+        }
+
+        @Override
+        public Void visitDereferenceExpression(
+            final DereferenceExpression node, final Void context) {
+          final QualifiedName qualifiedName = 
DereferenceExpression.getQualifiedName(node);
+          if (Objects.nonNull(qualifiedName)) {
+            collectUpdateValueColumn(
+                qualifiedName.getSuffix(), table, unknownColumnNames, 
invalidColumns);
+            return null;
+          }
+          return super.visitDereferenceExpression(node, context);
+        }
+      }.process(expression);
+
+      if (unknownColumnNames.isEmpty() && !invalidColumns.isEmpty()) {
+        final Map.Entry<String, TsTableColumnCategory> invalidColumn =
+            invalidColumns.entrySet().iterator().next();
+        throw new SemanticException(
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_CANNOT_REFERENCE_ARG_COLUMN_ARG_IN_AN_UPDATE_VALUE_UPDATE_VALUES_CAN_ONLY_REFERENCE_ATTRIBUTE_OR_TAG_COLUMNS_C01BE71A,
+                invalidColumn.getValue(),
+                invalidColumn.getKey()));
+      }
+    }
+
+    private void collectUpdateValueColumn(
+        final String columnName,
+        final TsTable table,
+        final Set<String> unknownColumnNames,
+        final Map<String, TsTableColumnCategory> invalidColumns) {
+      final TsTableColumnSchema columnSchema = 
table.getColumnSchema(columnName);
+      if (Objects.isNull(columnSchema)) {
+        unknownColumnNames.add(columnName);
+      } else if (columnSchema.getColumnCategory() == 
TsTableColumnCategory.FIELD
+          || columnSchema.getColumnCategory() == TsTableColumnCategory.TIME) {
+        invalidColumns.putIfAbsent(columnName, 
columnSchema.getColumnCategory());
+      }
+    }
+
     @Override
     public Scope visitDeleteDevice(final DeleteDevice node, final 
Optional<Scope> context) {
       // Actually write, but will return the result
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/impl/SchemaRegionMemoryImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/impl/SchemaRegionMemoryImpl.java
index 184c66cfc21..7c6fd4d7e53 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/impl/SchemaRegionMemoryImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/impl/SchemaRegionMemoryImpl.java
@@ -38,6 +38,7 @@ import org.apache.iotdb.commons.schema.SchemaConstant;
 import org.apache.iotdb.commons.schema.filter.SchemaFilterType;
 import org.apache.iotdb.commons.schema.node.role.IMeasurementMNode;
 import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
 import org.apache.iotdb.commons.schema.table.column.TsTableColumnSchema;
 import org.apache.iotdb.commons.schema.template.Template;
 import org.apache.iotdb.commons.schema.view.LogicalViewSchema;
@@ -52,6 +53,7 @@ import 
org.apache.iotdb.db.exception.metadata.PathNotExistException;
 import 
org.apache.iotdb.db.exception.metadata.SchemaDirCreationFailureException;
 import org.apache.iotdb.db.exception.metadata.SchemaQuotaExceededException;
 import org.apache.iotdb.db.exception.metadata.SeriesOverflowException;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.i18n.DataNodeSchemaMessages;
 import org.apache.iotdb.db.queryengine.common.schematree.ClusterSchemaTree;
 import 
org.apache.iotdb.db.queryengine.execution.operator.schema.source.DeviceAttributeUpdater;
@@ -64,6 +66,7 @@ import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.metadata.write.Cre
 import org.apache.iotdb.db.queryengine.plan.relational.metadata.Metadata;
 import 
org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.TableDeviceSchemaCache;
 import 
org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.TableId;
+import 
org.apache.iotdb.db.queryengine.plan.relational.planner.SymbolsExtractor;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.ConstructTableDevicesBlackListNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.CreateOrUpdateTableDeviceNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.DeleteTableDeviceNode;
@@ -74,6 +77,7 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.Table
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableDeviceAttributeUpdateNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableNodeLocationAddNode;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.DeleteDevice;
+import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.UpdateAssignment;
 import org.apache.iotdb.db.schemaengine.metric.ISchemaRegionMetric;
 import org.apache.iotdb.db.schemaengine.metric.SchemaRegionMemMetric;
 import org.apache.iotdb.db.schemaengine.rescon.DataNodeSchemaQuotaManager;
@@ -130,6 +134,7 @@ import 
org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
 import org.apache.iotdb.db.service.metrics.DataNodeExceptionMetrics;
 import org.apache.iotdb.db.storageengine.rescon.memory.SystemInfo;
 import org.apache.iotdb.db.utils.SchemaUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -1525,13 +1530,15 @@ public class SchemaRegionMemoryImpl implements 
ISchemaRegion {
   @Override
   public void updateTableDeviceAttribute(final TableDeviceAttributeUpdateNode 
updateNode)
       throws MetadataException {
-    try (final DeviceAttributeUpdater batchUpdater = 
constructDevicePredicateUpdater(updateNode)) {
+    final TsTable table =
+        DataNodeTableCache.getInstance()
+            .getTable(updateNode.getDatabase(), updateNode.getTableName());
+    final List<String> attributeNames = 
validateUpdateAttributeAssignments(updateNode, table);
+    try (final DeviceAttributeUpdater batchUpdater =
+        constructDevicePredicateUpdater(updateNode, table, attributeNames)) {
       for (final PartialPath pattern :
           TableDeviceQuerySource.getDevicePatternList(
-              updateNode.getDatabase(),
-              DataNodeTableCache.getInstance()
-                  .getTable(updateNode.getDatabase(), 
updateNode.getTableName()),
-              updateNode.getTagDeterminedFilterList())) {
+              updateNode.getDatabase(), table, 
updateNode.getTagDeterminedFilterList())) {
         mTree.updateTableDevice(pattern, batchUpdater);
       }
     }
@@ -1539,11 +1546,66 @@ public class SchemaRegionMemoryImpl implements 
ISchemaRegion {
     writeToMLog(updateNode);
   }
 
+  private List<String> validateUpdateAttributeAssignments(
+      final TableDeviceAttributeUpdateNode updateNode, final TsTable table)
+      throws MetadataException {
+    final List<String> attributeNames = new ArrayList<>();
+    final Set<String> uniqueAttributeNames = new HashSet<>();
+    for (final UpdateAssignment assignment : updateNode.getAssignments()) {
+      if (!(assignment.getName() instanceof SymbolReference)) {
+        throw new MetadataException(
+            DataNodeQueryMessages.UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS,
+            TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+      }
+
+      final String attributeName = ((SymbolReference) 
assignment.getName()).getName();
+      final TsTableColumnSchema columnSchema = 
table.getColumnSchema(attributeName);
+      if (Objects.nonNull(columnSchema)
+          && columnSchema.getColumnCategory() != 
TsTableColumnCategory.ATTRIBUTE) {
+        throw new MetadataException(
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_CANNOT_UPDATE_ARG_COLUMN_ARG_UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS_F805D1A6,
+                columnSchema.getColumnCategory(),
+                attributeName),
+            TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+      }
+      if (Objects.isNull(columnSchema)
+          || columnSchema.getColumnCategory() != 
TsTableColumnCategory.ATTRIBUTE) {
+        throw new MetadataException(
+            DataNodeQueryMessages.UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS,
+            TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+      }
+      if (!uniqueAttributeNames.add(attributeName)) {
+        throw new MetadataException(
+            
DataNodeQueryMessages.UPDATE_ATTRIBUTE_SHALL_SPECIFY_A_ATTRIBUTE_ONLY_ONCE,
+            TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+      }
+      for (final Symbol symbol : 
SymbolsExtractor.extractUnique(assignment.getValue())) {
+        final TsTableColumnSchema referencedColumnSchema = 
table.getColumnSchema(symbol.getName());
+        if (Objects.nonNull(referencedColumnSchema)
+            && (referencedColumnSchema.getColumnCategory() == 
TsTableColumnCategory.FIELD
+                || referencedColumnSchema.getColumnCategory() == 
TsTableColumnCategory.TIME)) {
+          throw new MetadataException(
+              String.format(
+                  DataNodeQueryMessages
+                      
.EXCEPTION_CANNOT_REFERENCE_ARG_COLUMN_ARG_IN_AN_UPDATE_VALUE_UPDATE_VALUES_CAN_ONLY_REFERENCE_ATTRIBUTE_OR_TAG_COLUMNS_C01BE71A,
+                  referencedColumnSchema.getColumnCategory(),
+                  symbol.getName()),
+              TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+        }
+      }
+      attributeNames.add(attributeName);
+    }
+    return attributeNames;
+  }
+
   private DeviceAttributeUpdater constructDevicePredicateUpdater(
-      final TableDeviceAttributeUpdateNode updateNode) {
+      final TableDeviceAttributeUpdateNode updateNode,
+      final TsTable table,
+      final List<String> attributeNames) {
     final String database = updateNode.getDatabase();
     final String tableName = updateNode.getTableName();
-    final TsTable table = DataNodeTableCache.getInstance().getTable(database, 
tableName);
     final Expression predicate = updateNode.getTagFuzzyPredicate();
     final List<TsTableColumnSchema> columnSchemaList =
         updateNode.getColumnHeaderList().stream()
@@ -1614,11 +1676,6 @@ public class SchemaRegionMemoryImpl implements 
ISchemaRegion {
             metadata,
             null);
 
-    final List<String> attributeNames =
-        updateNode.getAssignments().stream()
-            .map(assignment -> ((SymbolReference) 
assignment.getName()).getName())
-            .collect(Collectors.toList());
-
     // Project expressions don't contain Non-Mappable UDF, TransformOperator 
is not needed
     return new DeviceAttributeUpdater(
         filterOutputDataTypes,
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionSimpleRecoverTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionSimpleRecoverTest.java
index 6cc0fe06aee..d60a8f2cc3c 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionSimpleRecoverTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/metadata/schemaRegion/SchemaRegionSimpleRecoverTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.metadata.schemaRegion;
 
+import org.apache.iotdb.commons.exception.MetadataException;
 import org.apache.iotdb.commons.path.MeasurementPath;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.queryengine.common.SessionInfo;
@@ -30,10 +31,13 @@ import 
org.apache.iotdb.commons.schema.filter.impl.singlechild.TagFilter;
 import org.apache.iotdb.commons.schema.filter.impl.values.InFilter;
 import org.apache.iotdb.commons.schema.table.TsTable;
 import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
 import org.apache.iotdb.commons.schema.table.column.TagColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TimeColumnSchema;
 import org.apache.iotdb.commons.schema.template.Template;
 import org.apache.iotdb.consensus.ConsensusFactory;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.queryengine.common.schematree.ClusterSchemaTree;
 import 
org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableDeviceAttributeUpdateNode;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.UpdateAssignment;
@@ -318,4 +322,76 @@ public class SchemaRegionSimpleRecoverTest extends 
AbstractSchemaRegionTest {
         new Binary("value2", TSFileConfig.STRING_CHARSET),
         result.get(0).getAttributeValue(attributeName));
   }
+
+  @Test
+  public void testUpdateDeviceRejectsFieldAssignment() throws Exception {
+    if (!testParams.getTestModeName().equals("MemoryMode")) {
+      return;
+    }
+
+    final String database = "sg";
+    final String tableName = "t";
+    final TsTable testTable = new TsTable(tableName);
+    testTable.addColumnSchema(new TimeColumnSchema("time", 
TSDataType.TIMESTAMP));
+    testTable.addColumnSchema(new TagColumnSchema("tag", TSDataType.STRING));
+    testTable.addColumnSchema(new AttributeColumnSchema("attr", 
TSDataType.STRING));
+    testTable.addColumnSchema(new FieldColumnSchema("field", 
TSDataType.STRING));
+    DataNodeTableCache.getInstance().preUpdateTable(database, testTable, null);
+    DataNodeTableCache.getInstance().commitUpdateTable(database, tableName, 
null);
+
+    try {
+      final ISchemaRegion schemaRegion = getSchemaRegion(database, 0);
+      final MetadataException exception =
+          Assert.assertThrows(
+              MetadataException.class,
+              () ->
+                  schemaRegion.updateTableDeviceAttribute(
+                      new TableDeviceAttributeUpdateNode(
+                          new PlanNodeId(""),
+                          database,
+                          tableName,
+                          Collections.singletonList(Collections.emptyList()),
+                          null,
+                          Collections.singletonList(new ColumnHeader("tag", 
TSDataType.STRING)),
+                          null,
+                          Collections.singletonList(
+                              new UpdateAssignment(
+                                  new SymbolReference("field"), new 
StringLiteral("invalid"))),
+                          new SessionInfo(0, SessionConfig.DEFAULT_USER, 
ZoneId.systemDefault()))));
+      Assert.assertEquals(
+          String.format(
+              DataNodeQueryMessages
+                  
.EXCEPTION_CANNOT_UPDATE_ARG_COLUMN_ARG_UPDATE_CAN_ONLY_SPECIFY_ATTRIBUTE_COLUMNS_F805D1A6,
+              "FIELD",
+              "field"),
+          exception.getMessage());
+
+      final MetadataException valueException =
+          Assert.assertThrows(
+              MetadataException.class,
+              () ->
+                  schemaRegion.updateTableDeviceAttribute(
+                      new TableDeviceAttributeUpdateNode(
+                          new PlanNodeId(""),
+                          database,
+                          tableName,
+                          Collections.singletonList(Collections.emptyList()),
+                          null,
+                          Collections.singletonList(new ColumnHeader("tag", 
TSDataType.STRING)),
+                          null,
+                          Collections.singletonList(
+                              new UpdateAssignment(
+                                  new SymbolReference("attr"), new 
SymbolReference("field"))),
+                          new SessionInfo(0, SessionConfig.DEFAULT_USER, 
ZoneId.systemDefault()))));
+      Assert.assertEquals(
+          String.format(
+              DataNodeQueryMessages
+                  
.EXCEPTION_CANNOT_REFERENCE_ARG_COLUMN_ARG_IN_AN_UPDATE_VALUE_UPDATE_VALUES_CAN_ONLY_REFERENCE_ATTRIBUTE_OR_TAG_COLUMNS_C01BE71A,
+              "FIELD",
+              "field"),
+          valueException.getMessage());
+    } finally {
+      DataNodeTableCache.getInstance().invalid(database);
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
index 5b5a384f756..5cc6261b4a6 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/AnalyzerTest.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
 import org.apache.iotdb.common.rpc.thrift.TSeriesPartitionSlot;
 import org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot;
 import org.apache.iotdb.commons.conf.IoTDBConstant;
+import org.apache.iotdb.commons.exception.SemanticException;
 import org.apache.iotdb.commons.partition.DataPartition;
 import org.apache.iotdb.commons.partition.DataPartitionQueryParam;
 import org.apache.iotdb.commons.partition.executor.SeriesPartitionExecutor;
@@ -45,13 +46,19 @@ import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.OutputN
 import 
org.apache.iotdb.commons.queryengine.plan.relational.planner.node.ProjectNode;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Expression;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.LogicalExpression;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.QualifiedName;
 import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.StringLiteral;
+import 
org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.SymbolReference;
+import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Table;
 import 
org.apache.iotdb.commons.queryengine.plan.relational.type.InternalTypeManager;
 import org.apache.iotdb.commons.schema.filter.SchemaFilter;
 import org.apache.iotdb.commons.schema.table.InsertNodeMeasurementInfo;
 import org.apache.iotdb.commons.schema.table.TsTable;
 import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
 import org.apache.iotdb.commons.schema.table.column.TagColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TimeColumnSchema;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.protocol.session.InternalClientSession;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
@@ -78,6 +85,8 @@ import 
org.apache.iotdb.db.queryengine.plan.relational.security.AccessControl;
 import 
org.apache.iotdb.db.queryengine.plan.relational.security.AllowAllAccessControl;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AbstractQueryDeviceWithCache;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ShowDevice;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Update;
+import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.UpdateAssignment;
 import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.rewrite.StatementRewriteFactory;
 import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
@@ -137,6 +146,7 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 
@@ -1122,6 +1132,134 @@ public class AnalyzerTest {
     assertDeviceQueryCanBeAnalyzedAgain("COUNT DEVICES FROM table1 WHERE tag1 
= 'shanghai'");
   }
 
+  @Test
+  public void testPipeTransferredUpdateRejectsInvalidTarget() {
+    final String database = "testdb";
+    final TsTable tsTable = new TsTable(table);
+    tsTable.addColumnSchema(new TagColumnSchema("tag1", TSDataType.STRING));
+    tsTable.addColumnSchema(new AttributeColumnSchema("attr1", 
TSDataType.STRING));
+    tsTable.addColumnSchema(new FieldColumnSchema("s1", TSDataType.INT64));
+    DataNodeTableCache.getInstance().preUpdateTable(database, tsTable, null);
+    DataNodeTableCache.getInstance().commitUpdateTable(database, table, null);
+
+    try {
+      final Update statement =
+          new Update(
+              null,
+              new Table(QualifiedName.of(database, table)),
+              Collections.singletonList(
+                  new UpdateAssignment(new SymbolReference("s1"), new 
StringLiteral("invalid"))),
+              null);
+      final SessionInfo session =
+          new SessionInfo(0, "test", ZoneId.systemDefault(), database, 
SqlDialect.TABLE);
+      final MPPQueryContext queryContext =
+          new MPPQueryContext("", new QueryId("pipe_update_field"), session, 
null, null);
+
+      final SemanticException exception =
+          assertThrows(
+              SemanticException.class,
+              () ->
+                  analyzeStatement(
+                      statement, TEST_MATADATA, queryContext, new SqlParser(), 
session));
+      assertEquals(
+          "Cannot update FIELD column 's1'. UPDATE can only specify ATTRIBUTE 
columns.",
+          exception.getMessage());
+
+      final Update unknownColumnStatement =
+          new Update(
+              null,
+              new Table(QualifiedName.of(database, table)),
+              Collections.singletonList(
+                  new UpdateAssignment(
+                      new SymbolReference("missing_column"), new 
StringLiteral("invalid"))),
+              null);
+      final MPPQueryContext unknownColumnQueryContext =
+          new MPPQueryContext("", new QueryId("pipe_update_unknown_column"), 
session, null, null);
+
+      final SemanticException unknownColumnException =
+          assertThrows(
+              SemanticException.class,
+              () ->
+                  analyzeStatement(
+                      unknownColumnStatement,
+                      TEST_MATADATA,
+                      unknownColumnQueryContext,
+                      new SqlParser(),
+                      session));
+      assertEquals(
+          "Column 'missing_column' cannot be resolved", 
unknownColumnException.getMessage());
+    } finally {
+      DataNodeTableCache.getInstance().invalid(database);
+    }
+  }
+
+  @Test
+  public void testSqlUpdateRejectsInvalidColumnsWithExplicitErrors() {
+    final String database = "testdb";
+    final TsTable tsTable = new TsTable(table);
+    tsTable.addColumnSchema(new TimeColumnSchema("time", 
TSDataType.TIMESTAMP));
+    tsTable.addColumnSchema(new TagColumnSchema("tag1", TSDataType.STRING));
+    tsTable.addColumnSchema(new AttributeColumnSchema("attr1", 
TSDataType.STRING));
+    tsTable.addColumnSchema(new FieldColumnSchema("s1", TSDataType.INT64));
+    DataNodeTableCache.getInstance().preUpdateTable(database, tsTable, null);
+    DataNodeTableCache.getInstance().commitUpdateTable(database, table, null);
+
+    try {
+      assertUpdateSemanticException(
+          "UPDATE table1 SET S1 = 'invalid'",
+          "Cannot update FIELD column 's1'. UPDATE can only specify ATTRIBUTE 
columns.",
+          "sql_update_field_target");
+      assertUpdateSemanticException(
+          "UPDATE table1 SET tag1 = 'invalid'",
+          "Cannot update TAG column 'tag1'. UPDATE can only specify ATTRIBUTE 
columns.",
+          "sql_update_tag_target");
+      assertUpdateSemanticException(
+          "UPDATE table1 SET time = 1",
+          "Cannot update TIME column 'time'. UPDATE can only specify ATTRIBUTE 
columns.",
+          "sql_update_time_target");
+      assertUpdateSemanticException(
+          "UPDATE table1 SET attr1 = s1",
+          "Cannot reference FIELD column 's1' in an UPDATE value. UPDATE 
values can only reference ATTRIBUTE or TAG columns.",
+          "sql_update_field_value");
+      assertUpdateSemanticException(
+          "UPDATE table1 SET attr1 = time",
+          "Cannot reference TIME column 'time' in an UPDATE value. UPDATE 
values can only reference ATTRIBUTE or TAG columns.",
+          "sql_update_time_value");
+      assertUpdateSemanticExceptionContains(
+          "UPDATE table1 SET attr1 = missing_column",
+          "Column 'missing_column' is not an attribute or tag column",
+          "sql_update_unknown_value");
+
+      final String validSql = "UPDATE table1 SET attr1 = tag1";
+      assertNotNull(
+          analyzeSQL(
+              validSql,
+              TEST_MATADATA,
+              new MPPQueryContext(
+                  validSql, new QueryId("sql_update_tag_value"), sessionInfo, 
null, null)));
+    } finally {
+      DataNodeTableCache.getInstance().invalid(database);
+    }
+  }
+
+  private void assertUpdateSemanticException(
+      final String sql, final String expectedMessage, final String queryId) {
+    final MPPQueryContext queryContext =
+        new MPPQueryContext(sql, new QueryId(queryId), sessionInfo, null, 
null);
+    final SemanticException exception =
+        assertThrows(SemanticException.class, () -> analyzeSQL(sql, 
TEST_MATADATA, queryContext));
+    assertEquals(expectedMessage, exception.getMessage());
+  }
+
+  private void assertUpdateSemanticExceptionContains(
+      final String sql, final String expectedMessage, final String queryId) {
+    final MPPQueryContext queryContext =
+        new MPPQueryContext(sql, new QueryId(queryId), sessionInfo, null, 
null);
+    final SemanticException exception =
+        assertThrows(SemanticException.class, () -> analyzeSQL(sql, 
TEST_MATADATA, queryContext));
+    assertTrue(exception.getMessage(), 
exception.getMessage().contains(expectedMessage));
+  }
+
   @Test
   public void testInternalShowDeviceKeepsPreparedTraversalState() {
     final String database = "testdb";

Reply via email to