This is an automated email from the ASF dual-hosted git repository. jt2594838 pushed a commit to branch insert_numeric_row in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit f0bed1c20aeaf6fdc84087a24a044ccf3e7a5cbc Author: Tian Jiang <[email protected]> AuthorDate: Wed Sep 16 18:13:04 2026 +0800 tmp --- .../isession/NumericRowPreparedStatement.java | 132 +++++++++++ .../session/PreparedNumericRowBufferTest.java | 141 ++++++++++++ .../apache/iotdb/db/i18n/DataNodeMiscMessages.java | 22 ++ .../apache/iotdb/db/i18n/DataNodeMiscMessages.java | 22 ++ .../protocol/session/PreparedNumericRowInfo.java | 242 +++++++++++++++++++++ 5 files changed, 559 insertions(+) diff --git a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRowPreparedStatement.java b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRowPreparedStatement.java new file mode 100644 index 00000000000..8852d05a8a8 --- /dev/null +++ b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRowPreparedStatement.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. + */ +package org.apache.iotdb.isession; + +import org.apache.iotdb.isession.i18n.ISessionMessages; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Immutable metadata and reusable serialization buffers for repeated numeric table row inserts. + * + * <p>This statement is not thread-safe. An execution must return before the statement is reused or + * closed, because its buffers are retained until the synchronous RPC completes. + */ +public final class NumericRowPreparedStatement implements AutoCloseable { + private final String tableName; + private final List<String> columnNames; + private final List<TSDataType> dataTypes; + private final List<ColumnCategory> columnCategories; + private final int tagCount; + private final int valueBytes; + private ByteBuffer tagBuffer; + private ByteBuffer valueBuffer; + private long preparedId = -1; + private Object owner; + private Runnable closeAction = () -> {}; + private boolean closed; + + public NumericRowPreparedStatement( + String tableName, + List<String> columnNames, + List<TSDataType> dataTypes, + List<ColumnCategory> columnCategories) { + this.tableName = tableName; + this.columnNames = List.copyOf(columnNames); + this.dataTypes = List.copyOf(dataTypes); + this.columnCategories = List.copyOf(columnCategories); + // Column categories are immutable, so repeated inserts can reuse this count. + this.tagCount = + (int) this.columnCategories.stream().filter(c -> c == ColumnCategory.TAG).count(); + // FIELD encodings have fixed widths for the lifetime of the prepared schema. + int bytes = 0; + for (int i = 0; i < this.dataTypes.size(); i++) { + if (this.columnCategories.get(i) == ColumnCategory.FIELD) { + bytes += + switch (this.dataTypes.get(i)) { + case BOOLEAN -> 1; + case INT32, DATE, FLOAT -> Integer.BYTES; + case INT64, TIMESTAMP, DOUBLE -> Long.BYTES; + default -> + throw new IllegalArgumentException( + ISessionMessages.EXCEPTION_UNSUPPORTED_NUMERIC_TYPE_06DD3044); + }; + } + } + this.valueBytes = bytes; + } + + public String getTableName() { + return tableName; + } + + public List<String> getColumnNames() { + return columnNames; + } + + public List<TSDataType> getDataTypes() { + return dataTypes; + } + + public List<ColumnCategory> getColumnCategories() { + return columnCategories; + } + + public int getTagCount() { + return tagCount; + } + + /** + * Returns the TAG buffer cleared for writing, growing it when the encoded tags need more space. + */ + public ByteBuffer prepareTagBuffer(int requiredCapacity) { + if (tagBuffer == null || tagBuffer.capacity() < requiredCapacity) { + tagBuffer = ByteBuffer.allocate(requiredCapacity); + } + tagBuffer.clear(); + return tagBuffer; + } + + /** Returns the FIELD buffer cleared for writing; a prepared schema has a fixed encoded size. */ + public ByteBuffer prepareValueBuffer() { + if (valueBuffer == null) { + valueBuffer = ByteBuffer.allocate(valueBytes); + } + valueBuffer.clear(); + return valueBuffer; + } + + public long getPreparedId() { + return preparedId; + } + + public Object getOwner() { + return owner; + } + + public void bind(long id, Object owner, Runnable closeAction) { + this.preparedId = id; + this.owner = owner; + this.closeAction = closeAction; + this.closed = false; + } + + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + closeAction.run(); + closeAction = () -> {}; + closed = true; + } +} diff --git a/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowBufferTest.java b/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowBufferTest.java new file mode 100644 index 00000000000..a409d6844f0 --- /dev/null +++ b/iotdb-client/session/src/test/java/org/apache/iotdb/session/PreparedNumericRowBufferTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.session; + +import org.apache.iotdb.isession.NumericRowPreparedStatement; +import org.apache.iotdb.rpc.IoTDBConnectionException; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.ReadWriteIOUtils; +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PreparedNumericRowBufferTest { + @Test + public void testBuffersAcrossTagGrowthShrinkAndRpcFailure() throws Exception { + Session session = mock(Session.class); + SessionConnection connection = mock(SessionConnection.class); + when(session.getDefaultSessionConnection()).thenReturn(connection); + TableSession tableSession = new TableSession(session); + NumericRowPreparedStatement prepared = + new NumericRowPreparedStatement( + "t", + List.of("tag", "flag", "count", "value"), + List.of(TSDataType.STRING, TSDataType.BOOLEAN, TSDataType.INT32, TSDataType.DOUBLE), + List.of( + ColumnCategory.TAG, + ColumnCategory.FIELD, + ColumnCategory.FIELD, + ColumnCategory.FIELD)); + prepared.bind(1, connection, () -> {}); + List<ByteBuffer> tagBuffers = new ArrayList<>(); + List<ByteBuffer> valueBuffers = new ArrayList<>(); + String[] tags = {"x", "long-tag-\u4e2d\u6587", "", null, "retry"}; + doAnswer( + invocation -> { + int index = tagBuffers.size(); + ByteBuffer values = invocation.getArgument(2); + ByteBuffer encodedTags = invocation.getArgument(3); + tagBuffers.add(encodedTags); + valueBuffers.add(values); + // Consume both buffers as a transport may do, and verify no previous row bytes + // remain. + assertEquals(tags[index], ReadWriteIOUtils.readString(encodedTags)); + assertFalse(encodedTags.hasRemaining()); + assertEquals(13, values.remaining()); + assertEquals((byte) (index % 2), values.get()); + assertEquals(index, values.getInt()); + assertEquals(Double.doubleToRawLongBits(index + 0.5), values.getLong()); + assertFalse(values.hasRemaining()); + if (index == 3) { + throw new IoTDBConnectionException("simulated transport failure"); + } + return null; + }) + .when(connection) + .executePreparedNumericRow(eq(1L), anyLong(), any(ByteBuffer.class), any(ByteBuffer.class)); + for (int i = 0; i < tags.length; i++) { + try { + tableSession.executePreparedInsertNumericRow( + prepared, + i, + new long[] {i % 2, i, Double.doubleToRawLongBits(i + 0.5)}, + new String[] {tags[i]}); + assertNotEquals(3, i); + } catch (IoTDBConnectionException e) { + assertEquals(3, i); + } + } + assertEquals(tags.length, tagBuffers.size()); + assertNotSame(tagBuffers.get(0), tagBuffers.get(1)); + for (int i = 1; i < tags.length; i++) { + assertSame(valueBuffers.get(0), valueBuffers.get(i)); + assertSame(tagBuffers.get(1), tagBuffers.get(i)); + } + } + + @Test + public void testBuffersAreOwnedByEachStatementWithoutTags() throws Exception { + Session session = mock(Session.class); + SessionConnection connection = mock(SessionConnection.class); + when(session.getDefaultSessionConnection()).thenReturn(connection); + TableSession tableSession = new TableSession(session); + List<ByteBuffer> buffers = new ArrayList<>(); + doAnswer( + invocation -> { + ByteBuffer values = invocation.getArgument(2); + ByteBuffer tags = invocation.getArgument(3); + assertEquals(0, tags.remaining()); + assertEquals(42L, values.getLong()); + assertFalse(values.hasRemaining()); + buffers.add(values); + buffers.add(tags); + return null; + }) + .when(connection) + .executePreparedNumericRow( + anyLong(), anyLong(), any(ByteBuffer.class), any(ByteBuffer.class)); + for (int i = 0; i < 2; i++) { + NumericRowPreparedStatement prepared = + new NumericRowPreparedStatement( + "t", List.of("v"), List.of(TSDataType.INT64), List.of(ColumnCategory.FIELD)); + prepared.bind(i, connection, () -> {}); + tableSession.executePreparedInsertNumericRow(prepared, 0, new long[] {42}, new String[0]); + } + assertNotSame(buffers.get(0), buffers.get(2)); + assertNotSame(buffers.get(1), buffers.get(3)); + } +} diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java index d4ce7f97ba8..d7e2081e760 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java @@ -1490,5 +1490,27 @@ public final class DataNodeMiscMessages { "table_query_device_entry_batch_size_in_bytes (%d) exceeds the maximum RPC payload (dn_thrift_max_frame_size %d minus 1024 bytes); using %d as the effective value"; public static final String EXCEPTION_INVALID_PREPARED_NUMERIC_ROW_METADATA_OR_PAYLOAD_AECCCEAA = "Invalid prepared numeric row metadata or payload."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_REQUEST_IS_NULL_5A9F9BA9 = "Prepared numeric row request is null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATABASE_IS_NULL_F2A2E0AC = "Prepared numeric row database is null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TABLE_NAME_IS_NULL_OR_EMPTY_F2D38BE5 = "Prepared numeric row table name is null or empty."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_MEASUREMENTS_ARE_NULL_830DE646 = "Prepared numeric row measurements are null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPES_ARE_NULL_7C15F477 = "Prepared numeric row data types are null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORIES_ARE_NULL_6F2EE1BE = "Prepared numeric row column categories are null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_COLUMNS_D4E40365 = "Prepared numeric row has no columns."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_METADATA_SIZE_MISMATCH_MEASUREMENTS_ARG_DATA_TYPES_ARG_CATEGORIES_ARG_23DAB34B = "Prepared numeric row column metadata size mismatch: measurements %d, data types %d, categories %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_NULL_OR_EMPTY_AT_INDEX_ARG_DEBDCFF8 = "Prepared numeric row column name is null or empty at index %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_DUPLICATED_AT_INDEX_ARG_ARG_73AAFECC = "Prepared numeric row column name is duplicated at index %d: %s."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_IS_NULL_AT_INDEX_ARG_FB2A1FD6 = "Prepared numeric row data type is null at index %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_ORDINAL_IS_INVALID_AT_INDEX_ARG_ARG_BB5059BB = "Prepared numeric row data type ordinal is invalid at index %d: %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORY_IS_NULL_AT_INDEX_ARG_3ED8315E = "Prepared numeric row column category is null at index %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_IS_INVALID_AT_INDEX_ARG_CATEGORY_ARG_AND_TYPE_ARG_9DAEEA10 = "Prepared numeric row column is invalid at index %d: category %d and type %s."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_FIELD_COLUMNS_A76209AE = "Prepared numeric row has no FIELD columns."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_IS_NULL_3FF27BF7 = "Prepared numeric row tag buffer is null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_IS_NULL_A928F919 = "Prepared numeric row value buffer is null."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_SIZE_MISMATCH_EXPECTED_ARG_BYTES_BUT_GOT_ARG_B75D48C8 = "Prepared numeric row value buffer size mismatch: expected %d bytes but got %d."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_PREFIX_IS_INCOMPLETE_AT_INDEX_ARG_ARG_BYTES_REMAINING_6D342F3A = "Prepared numeric row tag length prefix is incomplete at index %d: %d bytes remaining."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_IS_INVALID_AT_INDEX_ARG_LENGTH_ARG_ARG_BYTES_REMAINING_DEDD1E8F = "Prepared numeric row tag length is invalid at index %d: length %d, %d bytes remaining."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_HAS_ARG_TRAILING_BYTES_042CF4D2 = "Prepared numeric row tag buffer has %d trailing bytes."; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_PAYLOAD_DECODING_FAILED_ARG_F87689DD = "Prepared numeric row payload decoding failed: %s."; public static final String EXCEPTION_PREPARED_NUMERIC_ROW_IS_CLOSED_OR_DOES_NOT_BELONG_TO_THIS_SESSION_5CC5C50A = "Prepared numeric row is closed or does not belong to this session."; } diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java index 2d19c74be8b..c8ecb24f9e8 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java @@ -1471,6 +1471,28 @@ public final class DataNodeMiscMessages { public static final String EXCEPTION_INVALID_PREPARED_NUMERIC_ROW_METADATA_OR_PAYLOAD_AECCCEAA = "预备数值行元数据或数据载荷无效"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_REQUEST_IS_NULL_5A9F9BA9 = "预备数值行请求为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATABASE_IS_NULL_F2A2E0AC = "预备数值行数据库为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TABLE_NAME_IS_NULL_OR_EMPTY_F2D38BE5 = "预备数值行表名为 null 或为空"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_MEASUREMENTS_ARE_NULL_830DE646 = "预备数值行 measurements 为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPES_ARE_NULL_7C15F477 = "预备数值行数据类型为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORIES_ARE_NULL_6F2EE1BE = "预备数值行列类别为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_COLUMNS_D4E40365 = "预备数值行没有列"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_METADATA_SIZE_MISMATCH_MEASUREMENTS_ARG_DATA_TYPES_ARG_CATEGORIES_ARG_23DAB34B = "预备数值行列元数据数量不匹配:measurements %d,数据类型 %d,列类别 %d"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_NULL_OR_EMPTY_AT_INDEX_ARG_DEBDCFF8 = "预备数值行列名在下标 %d 处为 null 或为空"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_DUPLICATED_AT_INDEX_ARG_ARG_73AAFECC = "预备数值行列名在下标 %d 处重复:%s"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_IS_NULL_AT_INDEX_ARG_FB2A1FD6 = "预备数值行数据类型在下标 %d 处为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_ORDINAL_IS_INVALID_AT_INDEX_ARG_ARG_BB5059BB = "预备数值行数据类型序号在下标 %d 处无效:%d"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORY_IS_NULL_AT_INDEX_ARG_3ED8315E = "预备数值行列类别在下标 %d 处为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_IS_INVALID_AT_INDEX_ARG_CATEGORY_ARG_AND_TYPE_ARG_9DAEEA10 = "预备数值行列在下标 %d 处无效:类别 %d,类型 %s"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_FIELD_COLUMNS_A76209AE = "预备数值行没有 FIELD 列"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_IS_NULL_3FF27BF7 = "预备数值行 TAG 缓冲区为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_IS_NULL_A928F919 = "预备数值行 FIELD 缓冲区为 null"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_SIZE_MISMATCH_EXPECTED_ARG_BYTES_BUT_GOT_ARG_B75D48C8 = "预备数值行 FIELD 缓冲区大小不匹配:期望 %d 字节,实际 %d 字节"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_PREFIX_IS_INCOMPLETE_AT_INDEX_ARG_ARG_BYTES_REMAINING_6D342F3A = "预备数值行 TAG 长度前缀在下标 %d 处不完整:剩余 %d 字节"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_IS_INVALID_AT_INDEX_ARG_LENGTH_ARG_ARG_BYTES_REMAINING_DEDD1E8F = "预备数值行 TAG 长度在下标 %d 处无效:长度 %d,剩余 %d 字节"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_HAS_ARG_TRAILING_BYTES_042CF4D2 = "预备数值行 TAG 缓冲区有 %d 个尾随字节"; + public static final String EXCEPTION_PREPARED_NUMERIC_ROW_PAYLOAD_DECODING_FAILED_ARG_F87689DD = "预备数值行数据载荷解码失败:%s"; public static final String EXCEPTION_PREPARED_NUMERIC_ROW_IS_CLOSED_OR_DOES_NOT_BELONG_TO_THIS_SESSION_5CC5C50A = "预备数值行已关闭或不属于当前会话"; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/PreparedNumericRowInfo.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/PreparedNumericRowInfo.java new file mode 100644 index 00000000000..a1a67e45100 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/session/PreparedNumericRowInfo.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.protocol.session; + +import org.apache.iotdb.commons.exception.SemanticException; +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.queryengine.utils.TimestampPrecisionUtils; +import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory; +import org.apache.iotdb.db.i18n.DataNodeMiscMessages; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; +import org.apache.iotdb.db.utils.NumericRowValues; +import org.apache.iotdb.service.rpc.thrift.TSPrepareNumericRowReq; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.RamUsageEstimator; +import org.apache.tsfile.utils.ReadWriteIOUtils; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Session-owned metadata. Each execution gets fresh arrays because schema analysis mutates them. + */ +public final class PreparedNumericRowInfo { + private final String database; + private final String table; + private final String[] measurements; + private final TSDataType[] types; + private final TsTableColumnCategory[] categories; + private final int tagCount; + private final int valueBytes; + + public PreparedNumericRowInfo(String database, TSPrepareNumericRowReq req) { + if (req == null) { + throw invalid(DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_REQUEST_IS_NULL_5A9F9BA9); + } + if (database == null) { + throw invalid(DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_DATABASE_IS_NULL_F2A2E0AC); + } + if (req.getTableName() == null || req.getTableName().isEmpty()) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_TABLE_NAME_IS_NULL_OR_EMPTY_F2D38BE5); + } + if (req.getMeasurements() == null) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_MEASUREMENTS_ARE_NULL_830DE646); + } + if (req.getDataTypes() == null) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPES_ARE_NULL_7C15F477); + } + if (req.getColumnCategories() == null) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORIES_ARE_NULL_6F2EE1BE); + } + if (req.getMeasurementsSize() == 0) { + throw invalid(DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_COLUMNS_D4E40365); + } + if (req.getMeasurementsSize() != req.getDataTypesSize() + || req.getMeasurementsSize() != req.getColumnCategoriesSize()) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_METADATA_SIZE_MISMATCH_MEASUREMENTS_ARG_DATA_TYPES_ARG_CATEGORIES_ARG_23DAB34B, + req.getMeasurementsSize(), + req.getDataTypesSize(), + req.getColumnCategoriesSize()); + } + this.database = database; + table = req.getTableName(); + measurements = req.getMeasurements().toArray(new String[0]); + types = new TSDataType[measurements.length]; + categories = new TsTableColumnCategory[measurements.length]; + int tags = 0; + int bytes = 0; + Set<String> names = new HashSet<>(); + for (int i = 0; i < measurements.length; i++) { + Integer type = req.getDataTypes().get(i); + Byte category = req.getColumnCategories().get(i); + if (measurements[i] == null || measurements[i].isEmpty()) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_NULL_OR_EMPTY_AT_INDEX_ARG_DEBDCFF8, + i); + } + if (!names.add(measurements[i].toLowerCase(Locale.ROOT))) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_NAME_IS_DUPLICATED_AT_INDEX_ARG_ARG_73AAFECC, + i, + measurements[i]); + } + if (type == null) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_IS_NULL_AT_INDEX_ARG_FB2A1FD6, + i); + } + if (type < 0 || type >= TSDataType.values().length) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_DATA_TYPE_ORDINAL_IS_INVALID_AT_INDEX_ARG_ARG_BB5059BB, + i, + type); + } + if (category == null) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_CATEGORY_IS_NULL_AT_INDEX_ARG_3ED8315E, + i); + } + types[i] = TSDataType.values()[type]; + if (category == ColumnCategory.TAG.ordinal() && types[i] == TSDataType.STRING && tags == i) { + tags++; + categories[i] = TsTableColumnCategory.TAG; + } else if (category == ColumnCategory.FIELD.ordinal() + && NumericRowValues.supports(types[i])) { + categories[i] = TsTableColumnCategory.FIELD; + bytes = Math.addExact(bytes, types[i].getDataTypeSize()); + } else { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_COLUMN_IS_INVALID_AT_INDEX_ARG_CATEGORY_ARG_AND_TYPE_ARG_9DAEEA10, + i, + category, + types[i]); + } + } + tagCount = tags; + valueBytes = bytes; + if (tagCount == measurements.length) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_HAS_NO_FIELD_COLUMNS_A76209AE); + } + } + + public InsertNumericRowStatement bind(long timestamp, ByteBuffer tags, ByteBuffer values) { + if (tags == null) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_IS_NULL_3FF27BF7); + } + if (values == null) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_IS_NULL_A928F919); + } + if (values.remaining() != valueBytes) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_VALUE_BUFFER_SIZE_MISMATCH_EXPECTED_ARG_BYTES_BUT_GOT_ARG_B75D48C8, + valueBytes, + values.remaining()); + } + TimestampPrecisionUtils.checkTimestampPrecision(timestamp); + NumericRowValues row = new NumericRowValues(tagCount, types.length - tagCount); + // Duplicate cursors so validation never changes a caller's buffers, even on failure. + tags = tags.duplicate(); + values = values.duplicate(); + try { + for (int i = 0; i < tagCount; i++) { + if (tags.remaining() < Integer.BYTES) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_PREFIX_IS_INCOMPLETE_AT_INDEX_ARG_ARG_BYTES_REMAINING_6D342F3A, + i, + tags.remaining()); + } + int length = tags.getInt(tags.position()); + if (length < -1 || length > tags.remaining() - Integer.BYTES) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_TAG_LENGTH_IS_INVALID_AT_INDEX_ARG_LENGTH_ARG_ARG_BYTES_REMAINING_DEDD1E8F, + i, + length, + tags.remaining()); + } + row.setTag(i, ReadWriteIOUtils.readString(tags)); + } + if (tags.hasRemaining()) { + throw invalid( + DataNodeMiscMessages + .EXCEPTION_PREPARED_NUMERIC_ROW_TAG_BUFFER_HAS_ARG_TRAILING_BYTES_042CF4D2, + tags.remaining()); + } + for (int i = tagCount; i < types.length; i++) { + row.read(i, types[i], values); + } + } catch (BufferUnderflowException e) { + throw invalid( + DataNodeMiscMessages.EXCEPTION_PREPARED_NUMERIC_ROW_PAYLOAD_DECODING_FAILED_ARG_F87689DD, + e.getMessage()); + } + InsertNumericRowStatement statement = new InsertNumericRowStatement(); + statement.setDevicePath(new PartialPath(table, false)); + statement.setDatabaseName(database); + statement.setMeasurements(measurements.clone()); + statement.setDataTypes(types.clone()); + statement.setColumnCategories(categories.clone()); + statement.setTime(timestamp); + statement.setNumericValues(row); + return statement; + } + + public long ramBytesUsed() { + return RamUsageEstimator.shallowSizeOfInstance(PreparedNumericRowInfo.class) + + RamUsageEstimator.sizeOf(database) + + RamUsageEstimator.sizeOf(table) + + RamUsageEstimator.sizeOf(measurements) + + RamUsageEstimator.shallowSizeOf(types) + + RamUsageEstimator.shallowSizeOf(categories) + // Account for the map entry and boxed ID retained by the session. + + 64; + } + + private static SemanticException invalid() { + return new SemanticException( + DataNodeMiscMessages.EXCEPTION_INVALID_PREPARED_NUMERIC_ROW_METADATA_OR_PAYLOAD_AECCCEAA); + } + + private static SemanticException invalid(String message, Object... args) { + return new SemanticException(String.format(message, args)); + } +}
