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";