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 7a8079d1524 Table: Ignore null attribute values in insert (#17790)
7a8079d1524 is described below
commit 7a8079d1524cbd280e3b2a689f203340e6838794
Author: Caideyipi <[email protected]>
AuthorDate: Tue Jun 2 15:06:53 2026 +0800
Table: Ignore null attribute values in insert (#17790)
---
.../relational/it/db/it/IoTDBInsertTableIT.java | 35 +++++++++++
.../fetcher/TableDeviceSchemaValidator.java | 44 +++++++++++++-
.../plan/relational/sql/ast/InsertTablet.java | 17 ++++--
.../fetcher/TableDeviceSchemaValidatorTest.java | 29 ++++++++-
.../plan/relational/sql/ast/InsertTabletTest.java | 69 ++++++++++++++++++++++
5 files changed, 185 insertions(+), 9 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
index 3de74f71bf9..802d388c08d 100644
---
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
@@ -1018,6 +1018,41 @@ public class IoTDBInsertTableIT {
}
}
+ @Test
+ public void testInsertNullAttributeDoesNotOverwriteDeviceAttribute() throws
SQLException {
+ try (Connection connection =
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+ Statement statement = connection.createStatement()) {
+ statement.execute("use \"test\"");
+ statement.execute(
+ "create table attr_null_insert(tag1 string tag, attr1 string
attribute, attr2 string attribute, s1 int32 field)");
+
+ statement.execute(
+ "insert into attr_null_insert(time, tag1, attr1, attr2, s1)
values(1, 'd1', 'old1', 'old2', 1)");
+ statement.execute(
+ "insert into attr_null_insert(time, tag1, attr1, attr2, s1)
values(2, 'd1', null, null, 2)");
+ assertDeviceAttributes(statement, "old1", "old2");
+
+ statement.execute(
+ "insert into attr_null_insert(time, tag1, attr1, attr2, s1)
values(3, 'd1', null, 'new2', 3)");
+ assertDeviceAttributes(statement, "old1", "new2");
+
+ statement.execute("update attr_null_insert set attr1 = null where tag1 =
'd1'");
+ assertDeviceAttributes(statement, null, "new2");
+ }
+ }
+
+ private void assertDeviceAttributes(
+ final Statement statement, final String expectedAttr1, final String
expectedAttr2)
+ throws SQLException {
+ try (final ResultSet resultSet = statement.executeQuery("show devices from
attr_null_insert")) {
+ assertTrue(resultSet.next());
+ assertEquals("d1", resultSet.getString("tag1"));
+ assertEquals(expectedAttr1, resultSet.getString("attr1"));
+ assertEquals(expectedAttr2, resultSet.getString("attr2"));
+ assertFalse(resultSet.next());
+ }
+ }
+
@Test
public void testInsertWithTTL() {
try (Connection connection =
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidator.java
index 5c95fbb62d0..e783b4589c5 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidator.java
@@ -36,6 +36,7 @@ import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.tsfile.file.metadata.IDeviceID;
import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -74,7 +75,9 @@ public class TableDeviceSchemaValidator {
// High-cost operations, shall only be called once
final List<Object[]> deviceIdList = schemaValidation.getDeviceIdList();
final List<String> attributeKeyList =
schemaValidation.getAttributeColumnNameList();
- final List<Object[]> attributeValueList =
schemaValidation.getAttributeValueList();
+ // In normal inserts, null attributes mean no-op. Raw null is reserved for
explicit clears.
+ final List<Object[]> attributeValueList =
+
normalizeNullAttributeValuesForInsert(schemaValidation.getAttributeValueList());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
@@ -194,6 +197,9 @@ public class TableDeviceSchemaValidator {
for (int j = 0, size = attributeKeyList.size(); j < size; j++) {
final Object inputValue =
deviceAttributeValueList.length > j ? deviceAttributeValueList[j] :
null;
+ if (inputValue == null || inputValue == Constants.NONE) {
+ continue;
+ }
if (!Objects.equals(attributeMap.get(attributeKeyList.get(j)),
inputValue)) {
return true;
}
@@ -201,6 +207,42 @@ public class TableDeviceSchemaValidator {
return false;
}
+ private List<Object[]> normalizeNullAttributeValuesForInsert(
+ final List<Object[]> attributeValueList) {
+ List<Object[]> result = null;
+ for (int i = 0; i < attributeValueList.size(); i++) {
+ final Object[] deviceAttributeValueList = attributeValueList.get(i);
+ final Object[] normalizedAttributeValueList =
+ normalizeNullAttributeValuesForInsert(deviceAttributeValueList);
+ if (result != null) {
+ result.add(normalizedAttributeValueList);
+ } else if (normalizedAttributeValueList != deviceAttributeValueList) {
+ result = new ArrayList<>(attributeValueList.size());
+ for (int j = 0; j < i; j++) {
+ result.add(attributeValueList.get(j));
+ }
+ result.add(normalizedAttributeValueList);
+ }
+ }
+ return result == null ? attributeValueList : result;
+ }
+
+ private Object[] normalizeNullAttributeValuesForInsert(final Object[]
deviceAttributeValueList) {
+ for (int i = 0; i < deviceAttributeValueList.length; i++) {
+ if (deviceAttributeValueList[i] == null) {
+ final Object[] result =
+ Arrays.copyOf(deviceAttributeValueList,
deviceAttributeValueList.length);
+ for (int j = i; j < result.length; j++) {
+ if (result[j] == null) {
+ result[j] = Constants.NONE;
+ }
+ }
+ return result;
+ }
+ }
+ return deviceAttributeValueList;
+ }
+
private void autoCreateOrUpdateDeviceSchema(
final ITableDeviceSchemaValidation schemaValidation,
final ValidateResult previousValidateResult,
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
index f77bd3aa143..5d25b01ecf3 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
@@ -90,10 +90,17 @@ public class InsertTablet extends WrappedInsertStatement {
public List<Object[]> getAttributeValueList() {
prepareDeviceID2LastIdxMap();
final InsertTabletStatement insertTabletStatement =
getInnerTreeStatement();
- List<Object[]> result = new
ArrayList<>(insertTabletStatement.getRowCount());
final List<Integer> attrColumnIndices =
insertTabletStatement.getAttrColumnIndices();
- for (Integer rowIndex : deviceID2LastIdxMap.values()) {
- Object[] attrValues = new Object[attrColumnIndices.size()];
+
+ final Map<IDeviceID, Object[]> deviceID2AttributeValues =
+ new LinkedHashMap<>(deviceID2LastIdxMap.size());
+ for (final IDeviceID deviceID : deviceID2LastIdxMap.keySet()) {
+ deviceID2AttributeValues.put(deviceID, new
Object[attrColumnIndices.size()]);
+ }
+
+ for (int rowIndex = 0; rowIndex < insertTabletStatement.getRowCount();
rowIndex++) {
+ final Object[] attrValues =
+
deviceID2AttributeValues.get(insertTabletStatement.getTableDeviceID(rowIndex));
for (int attrColNum = 0; attrColNum < attrColumnIndices.size();
attrColNum++) {
final int columnIndex = attrColumnIndices.get(attrColNum);
if (!insertTabletStatement.isNull(rowIndex, columnIndex)) {
@@ -101,9 +108,9 @@ public class InsertTablet extends WrappedInsertStatement {
((Object[])
insertTabletStatement.getColumns()[columnIndex])[rowIndex];
}
}
- result.add(attrValues);
}
- return result;
+
+ return new ArrayList<>(deviceID2AttributeValues.values());
}
// The map cannot be maintained during construction because the IDeviceID
may be reset later.
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidatorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidatorTest.java
index 6242ab8960b..32fed0a8d08 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidatorTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableDeviceSchemaValidatorTest.java
@@ -20,6 +20,7 @@
package org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher;
import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.utils.Constants;
import org.junit.Assert;
import org.junit.Test;
@@ -32,17 +33,39 @@ import java.util.Map;
public class TableDeviceSchemaValidatorTest {
@Test
- public void testNullAttributeNeedsUpdate() {
+ public void testNullAttributeSkipsUpdate() {
final Map<String, Binary> attributeMap = new HashMap<>();
attributeMap.put("attr", new Binary("x", StandardCharsets.UTF_8));
- Assert.assertTrue(
+ Assert.assertFalse(
TableDeviceSchemaValidator.isAttributeUpdateRequired(
Collections.singletonList("attr"), new Object[] {null},
attributeMap));
}
@Test
- public void testMissingTailAttributeValueEqualsNull() {
+ public void testNoneAttributeSkipsUpdate() {
+ final Map<String, Binary> attributeMap = new HashMap<>();
+ attributeMap.put("attr", new Binary("x", StandardCharsets.UTF_8));
+
+ Assert.assertFalse(
+ TableDeviceSchemaValidator.isAttributeUpdateRequired(
+ Collections.singletonList("attr"), new Object[] {Constants.NONE},
attributeMap));
+ }
+
+ @Test
+ public void testNonNullAttributeNeedsUpdate() {
+ final Map<String, Binary> attributeMap = new HashMap<>();
+ attributeMap.put("attr", new Binary("x", StandardCharsets.UTF_8));
+
+ Assert.assertTrue(
+ TableDeviceSchemaValidator.isAttributeUpdateRequired(
+ Collections.singletonList("attr"),
+ new Object[] {new Binary("y", StandardCharsets.UTF_8)},
+ attributeMap));
+ }
+
+ @Test
+ public void testMissingTailAttributeValueSkipsUpdate() {
final Map<String, Binary> attributeMap = new HashMap<>();
attributeMap.put("attr1", new Binary("x", StandardCharsets.UTF_8));
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTabletTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTabletTest.java
index a78fabeb865..f16e680502a 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTabletTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTabletTest.java
@@ -127,4 +127,73 @@ public class InsertTabletTest {
assertArrayEquals(new Object[] {"id3_1"}, deviceIdList.get(2));
assertArrayEquals(new Object[] {}, deviceIdList.get(3));
}
+
+ @Test
+ public void testDuplicateDeviceUsesLastNonNullAttributeValue() {
+ InsertTabletStatement innerStmt = new InsertTabletStatement();
+ innerStmt.setDevicePath(new PartialPath("table1", false));
+ innerStmt.setTimes(new long[] {1, 2, 3, 4});
+ innerStmt.setRowCount(4);
+ innerStmt.setMeasurements(new String[] {"deviceId", "attr1", "attr2",
"measurement"});
+ innerStmt.setColumnCategories(
+ new TsTableColumnCategory[] {
+ TsTableColumnCategory.TAG,
+ TsTableColumnCategory.ATTRIBUTE,
+ TsTableColumnCategory.ATTRIBUTE,
+ TsTableColumnCategory.FIELD
+ });
+ innerStmt.setDataTypes(
+ new TSDataType[] {
+ TSDataType.STRING, TSDataType.STRING, TSDataType.STRING,
TSDataType.STRING
+ });
+ innerStmt.setColumns(
+ new Object[] {
+ new Binary[] {
+ new Binary("d1", StandardCharsets.UTF_8),
+ new Binary("d1", StandardCharsets.UTF_8),
+ new Binary("d1", StandardCharsets.UTF_8),
+ new Binary("d1", StandardCharsets.UTF_8)
+ },
+ new Binary[] {
+ new Binary("attr1_1", StandardCharsets.UTF_8),
+ Binary.EMPTY_VALUE,
+ new Binary("attr1_3", StandardCharsets.UTF_8),
+ Binary.EMPTY_VALUE
+ },
+ new Binary[] {
+ new Binary("attr2_1", StandardCharsets.UTF_8),
+ new Binary("attr2_2", StandardCharsets.UTF_8),
+ Binary.EMPTY_VALUE,
+ Binary.EMPTY_VALUE
+ },
+ new Binary[] {
+ new Binary("m1", StandardCharsets.UTF_8),
+ new Binary("m2", StandardCharsets.UTF_8),
+ new Binary("m3", StandardCharsets.UTF_8),
+ new Binary("m4", StandardCharsets.UTF_8)
+ },
+ });
+ innerStmt.setBitMaps(
+ new BitMap[] {
+ new BitMap(4, new byte[] {0x00}),
+ new BitMap(4, new byte[] {1 << 1 | 1 << 3}),
+ new BitMap(4, new byte[] {1 << 2 | 1 << 3}),
+ new BitMap(4, new byte[] {0x00}),
+ });
+
+ InsertTablet insertTablet = new InsertTablet(innerStmt, null);
+ assertEquals(Arrays.asList("attr1", "attr2"),
insertTablet.getAttributeColumnNameList());
+ List<Object[]> attributeValueList = insertTablet.getAttributeValueList();
+ assertEquals(1, attributeValueList.size());
+ assertArrayEquals(
+ new Object[] {
+ new Binary("attr1_3", StandardCharsets.UTF_8),
+ new Binary("attr2_2", StandardCharsets.UTF_8)
+ },
+ attributeValueList.get(0));
+
+ List<Object[]> deviceIdList = insertTablet.getDeviceIdList();
+ assertEquals(1, deviceIdList.size());
+ assertArrayEquals(new Object[] {"d1"}, deviceIdList.get(0));
+ }
}