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 eed7ef0dba99bf2ec390be12b1ff53ca193db2fb Author: Tian Jiang <[email protected]> AuthorDate: Fri Sep 11 12:30:16 2026 +0800 tmp --- .../IoTDBNumericRowInsertPerformanceIT.java | 456 +++++++++++++++++++++ .../it/session/IoTDBTableModelSessionIT.java | 75 ++-- .../org/apache/iotdb/isession/ITableSession.java | 16 +- .../java/org/apache/iotdb/isession/NumericRow.java | 97 +++++ .../apache/iotdb/session/i18n/SessionMessages.java | 4 + .../apache/iotdb/session/i18n/SessionMessages.java | 4 + .../java/org/apache/iotdb/session/Session.java | 51 +++ .../org/apache/iotdb/session/TableSession.java | 6 + .../iotdb/session/pool/TableSessionWrapper.java | 13 + .../apache/iotdb/session/util/SessionUtils.java | 74 ++++ .../java/org/apache/iotdb/session/SessionTest.java | 80 ++++ .../iotdb/session/util/SessionUtilsTest.java | 151 +++++++ .../tablet/PipeInsertNodeTabletInsertionEvent.java | 12 +- .../tablet/parser/TabletInsertionEventParser.java | 19 +- .../TabletInsertionEventTablePatternParser.java | 5 +- .../visitor/PipePlanToStatementVisitor.java | 15 + .../request/PipeTransferTabletBatchReqV2.java | 10 +- .../request/PipeTransferTabletBinaryReq.java | 4 +- .../request/PipeTransferTabletBinaryReqV2.java | 4 +- .../request/PipeTransferTabletInsertNodeReq.java | 4 +- .../request/PipeTransferTabletInsertNodeReqV2.java | 4 +- .../execution/executor/RegionWriteExecutor.java | 7 + .../plan/parser/StatementGenerator.java | 46 ++- .../plan/planner/plan/node/PlanVisitor.java | 5 + .../plan/node/write/AbstractInsertRowNode.java | 64 +++ .../relational/analyzer/StatementAnalyzer.java | 6 + .../plan/relational/planner/RelationPlanner.java | 23 ++ .../plan/relational/planner/TableModelPlanner.java | 14 + .../plan/relational/sql/ast/InsertNumericRow.java | 71 ++++ .../queryengine/plan/statement/StatementType.java | 1 + .../plan/statement/StatementVisitor.java | 5 + .../statement/crud/InsertNumericRowStatement.java | 292 +++++++++++++ .../iotdb/db/storageengine/StorageEngine.java | 4 +- .../dataregion/memtable/TsFileProcessor.java | 55 +-- .../dataregion/wal/buffer/WALEntry.java | 6 +- .../dataregion/wal/buffer/WALInfoEntry.java | 4 +- .../dataregion/wal/node/IWALNode.java | 4 +- .../dataregion/wal/node/WALFakeNode.java | 4 +- .../storageengine/dataregion/wal/node/WALNode.java | 4 +- .../wal/recover/file/TsFilePlanRedoer.java | 9 +- .../plan/parser/StatementGeneratorTest.java | 2 +- .../AlignedWritableMemChunkNumericTest.java | 134 ++++++ .../plan/planner/plan/node/PlanNodeType.java | 1 + 43 files changed, 1767 insertions(+), 98 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/performance/IoTDBNumericRowInsertPerformanceIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/performance/IoTDBNumericRowInsertPerformanceIT.java new file mode 100644 index 00000000000..84a52da1451 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/performance/IoTDBNumericRowInsertPerformanceIT.java @@ -0,0 +1,456 @@ +/* + * 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.relational.it.performance; + +import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; +import org.apache.iotdb.isession.SessionDataSet; +import org.apache.iotdb.it.env.EnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.TableLocalStandaloneIT; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.write.record.Tablet; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + * Manual end-to-end benchmark for the numeric table-row API and relational {@link Tablet} writes. + * + * <p>Enable this test with {@code -Diotdb.numeric.row.insert.perf.enabled=true}. The timed region + * includes the Session call and server write confirmation. Schema creation and client-side input + * preparation are outside the timed region. By default both APIs write one row per request; set + * {@code iotdb.numeric.row.insert.perf.tablet.row.count} to compare against batched Tablet writes. + * + * <p>Results are written to {@code target/numeric-row-insert-performance.properties}. Percentiles + * describe elapsed time per complete round, not individual RPC latency. See the adjacent README for + * reproducible commands and interpretation. + */ +@RunWith(IoTDBTestRunner.class) +@Category(TableLocalStandaloneIT.class) +public class IoTDBNumericRowInsertPerformanceIT { + + private static final String PROPERTY_PREFIX = "iotdb.numeric.row.insert.perf."; + private static final String ENABLED_PROPERTY = PROPERTY_PREFIX + "enabled"; + private static final String ROW_COUNT_PROPERTY = PROPERTY_PREFIX + "row.count"; + private static final String COLUMN_COUNT_PROPERTY = PROPERTY_PREFIX + "column.count"; + private static final String TABLET_ROW_COUNT_PROPERTY = PROPERTY_PREFIX + "tablet.row.count"; + private static final String WARMUP_ROUND_COUNT_PROPERTY = PROPERTY_PREFIX + "warmup.round.count"; + private static final String ROUND_COUNT_PROPERTY = PROPERTY_PREFIX + "round.count"; + private static final String RESULT_PATH_PROPERTY = PROPERTY_PREFIX + "result.path"; + + private static final int ROW_COUNT = Integer.getInteger(ROW_COUNT_PROPERTY, 10_000); + private static final int COLUMN_COUNT = Integer.getInteger(COLUMN_COUNT_PROPERTY, 8); + private static final int TABLET_ROW_COUNT = Integer.getInteger(TABLET_ROW_COUNT_PROPERTY, 1); + private static final int WARMUP_ROUND_COUNT = Integer.getInteger(WARMUP_ROUND_COUNT_PROPERTY, 1); + private static final int ROUND_COUNT = Integer.getInteger(ROUND_COUNT_PROPERTY, 5); + + private static final String DATABASE = "numeric_row_insert_performance"; + private static final List<TSDataType> TYPE_SEQUENCE = + Arrays.asList( + TSDataType.BOOLEAN, + TSDataType.INT32, + TSDataType.INT64, + TSDataType.FLOAT, + TSDataType.DOUBLE); + private static List<String> columnNames; + private static List<TSDataType> dataTypes; + private static List<ColumnCategory> columnCategories; + + private static boolean environmentInitialized; + + @BeforeClass + public static void setUp() throws Exception { + Assume.assumeTrue(Boolean.getBoolean(ENABLED_PROPERTY)); + // Validate before allocating inputs or starting nodes, including when a count is negative. + assertValidConfiguration(); + columnNames = createColumnNames(); + dataTypes = createDataTypes(); + columnCategories = Collections.nCopies(COLUMN_COUNT, ColumnCategory.FIELD); + environmentInitialized = true; + EnvFactory.getEnv().initClusterEnvironment(); + } + + @AfterClass + public static void tearDown() throws Exception { + if (environmentInitialized) { + EnvFactory.getEnv().cleanClusterEnvironment(); + } + } + + @Test + // Compare identical, prebuilt numeric data; verify every field count outside the timed calls. + public void testNumericRowInsertPerformanceAgainstTablet() throws Exception { + final long[] numericRowElapsedNanos = new long[ROUND_COUNT]; + final long[] tabletElapsedNanos = new long[ROUND_COUNT]; + + try (ITableSession session = EnvFactory.getEnv().getTableSessionConnection()) { + session.executeNonQueryStatement("CREATE DATABASE " + DATABASE); + session.executeNonQueryStatement("USE " + DATABASE); + + final int totalRoundCount = WARMUP_ROUND_COUNT + ROUND_COUNT; + for (int round = 0; round < totalRoundCount; round++) { + final NumericRowWorkload numericRowWorkload = prepareNumericRowWorkload(session, round); + final TabletWorkload tabletWorkload = prepareTabletWorkload(session, round); + + final long numericRowNanos; + final long tabletNanos; + // Alternate execution order so one path does not consistently receive a warmer server. + if ((round & 1) == 0) { + numericRowNanos = executeNumericRowWorkload(session, numericRowWorkload); + tabletNanos = executeTabletWorkload(session, tabletWorkload); + } else { + tabletNanos = executeTabletWorkload(session, tabletWorkload); + numericRowNanos = executeNumericRowWorkload(session, numericRowWorkload); + } + assertRowCount(session, numericRowWorkload.tableName); + assertRowCount(session, tabletWorkload.tableName); + + if (round >= WARMUP_ROUND_COUNT) { + final int measuredRound = round - WARMUP_ROUND_COUNT; + numericRowElapsedNanos[measuredRound] = numericRowNanos; + tabletElapsedNanos[measuredRound] = tabletNanos; + } + } + } + + final BenchmarkResult result = new BenchmarkResult(numericRowElapsedNanos, tabletElapsedNanos); + writeResult(result); + } + + private static NumericRowWorkload prepareNumericRowWorkload(ITableSession session, int round) + throws Exception { + final String tableName = "numeric_row_" + round; + createTable(session, tableName); + final long startTimestamp = (long) round * ROW_COUNT; + final NumericRow[] rows = new NumericRow[ROW_COUNT]; + final String[] tagValues = new String[0]; + for (int row = 0; row < ROW_COUNT; row++) { + rows[row] = + new NumericRow( + tableName, + startTimestamp + row, + columnNames, + dataTypes, + columnCategories, + createRawValues(startTimestamp + row), + tagValues); + } + return new NumericRowWorkload(tableName, rows); + } + + private static TabletWorkload prepareTabletWorkload(ITableSession session, int round) + throws Exception { + final String tableName = "tablet_" + round; + createTable(session, tableName); + final long startTimestamp = (long) round * ROW_COUNT; + final List<Tablet> tablets = new ArrayList<>((ROW_COUNT - 1) / TABLET_ROW_COUNT + 1); + for (int rowStart = 0; rowStart < ROW_COUNT; ) { + final int rowCount = Math.min(TABLET_ROW_COUNT, ROW_COUNT - rowStart); + final Tablet tablet = + new Tablet(tableName, columnNames, dataTypes, columnCategories, rowCount); + for (int row = 0; row < rowCount; row++) { + final int rowIndex = tablet.getRowSize(); + final long timestamp = startTimestamp + rowStart + row; + final long[] values = createRawValues(timestamp); + tablet.addTimestamp(rowIndex, timestamp); + for (int column = 0; column < COLUMN_COUNT; column++) { + tablet.addValue( + columnNames.get(column), + rowIndex, + toTabletValue(dataTypes.get(column), values[column])); + } + } + tablets.add(tablet); + rowStart += rowCount; + } + return new TabletWorkload(tableName, tablets); + } + + private static long executeNumericRowWorkload( + ITableSession session, NumericRowWorkload numericRowWorkload) throws Exception { + final long startNanos = System.nanoTime(); + for (final NumericRow row : numericRowWorkload.rows) { + session.insert(row); + } + return System.nanoTime() - startNanos; + } + + private static long executeTabletWorkload(ITableSession session, TabletWorkload tabletWorkload) + throws Exception { + final long startNanos = System.nanoTime(); + for (final Tablet tablet : tabletWorkload.tablets) { + session.insert(tablet); + } + return System.nanoTime() - startNanos; + } + + private static void createTable(ITableSession session, String tableName) throws Exception { + final StringBuilder statement = new StringBuilder("CREATE TABLE "); + statement.append(tableName).append('('); + for (int column = 0; column < COLUMN_COUNT; column++) { + if (column > 0) { + statement.append(','); + } + statement + .append(columnNames.get(column)) + .append(' ') + .append(dataTypes.get(column)) + .append(" FIELD"); + } + statement.append(')'); + session.executeNonQueryStatement(statement.toString()); + } + + private static void assertRowCount(ITableSession session, String tableName) throws Exception { + final List<String> counts = new ArrayList<>(COLUMN_COUNT); + for (String columnName : columnNames) { + counts.add("COUNT(" + columnName + ")"); + } + try (SessionDataSet dataSet = + session.executeQueryStatement( + "SELECT " + String.join(",", counts) + " FROM " + tableName)) { + Assert.assertTrue(dataSet.hasNext()); + for (var field : dataSet.next().getFields()) { + Assert.assertEquals(ROW_COUNT, field.getLongV()); + } + Assert.assertFalse(dataSet.hasNext()); + } + } + + private static long[] createRawValues(long timestamp) { + final long[] values = new long[COLUMN_COUNT]; + for (int column = 0; column < COLUMN_COUNT; column++) { + final TSDataType type = dataTypes.get(column); + // Floating-point values occupy raw IEEE 754 bits in the numeric-row API. + values[column] = + switch (type) { + case BOOLEAN -> (timestamp + column) & 1; + case INT32 -> (int) (timestamp + column); + case INT64 -> timestamp + column; + case FLOAT -> Float.floatToRawIntBits((float) timestamp + column / 10.0F); + case DOUBLE -> Double.doubleToRawLongBits(timestamp + column / 10.0D); + default -> throw new AssertionError(type); + }; + } + return values; + } + + private static Object toTabletValue(TSDataType type, long value) { + return switch (type) { + case BOOLEAN -> value != 0; + case INT32 -> (int) value; + case INT64 -> value; + case FLOAT -> Float.intBitsToFloat((int) value); + case DOUBLE -> Double.longBitsToDouble(value); + default -> throw new AssertionError(type); + }; + } + + private static List<String> createColumnNames() { + final List<String> columnNames = new ArrayList<>(COLUMN_COUNT); + for (int column = 0; column < COLUMN_COUNT; column++) { + columnNames.add("v" + column); + } + return Collections.unmodifiableList(columnNames); + } + + private static List<TSDataType> createDataTypes() { + final List<TSDataType> dataTypes = new ArrayList<>(COLUMN_COUNT); + for (int column = 0; column < COLUMN_COUNT; column++) { + dataTypes.add(TYPE_SEQUENCE.get(column % TYPE_SEQUENCE.size())); + } + return Collections.unmodifiableList(dataTypes); + } + + private static void assertValidConfiguration() { + Assert.assertTrue(ROW_COUNT > 0); + Assert.assertTrue(COLUMN_COUNT > 0); + Assert.assertTrue(TABLET_ROW_COUNT > 0); + Assert.assertTrue(WARMUP_ROUND_COUNT >= 0); + Assert.assertTrue(ROUND_COUNT > 0); + Assert.assertTrue((long) WARMUP_ROUND_COUNT + ROUND_COUNT <= Integer.MAX_VALUE); + } + + private static void writeResult(BenchmarkResult result) throws IOException { + final Properties properties = new Properties(); + properties.setProperty("format.version", "2"); + properties.setProperty("java.version", System.getProperty("java.version")); + properties.setProperty("os.name", System.getProperty("os.name")); + properties.setProperty("os.arch", System.getProperty("os.arch")); + properties.setProperty( + "client.available.processors", + Integer.toString(Runtime.getRuntime().availableProcessors())); + properties.setProperty( + "client.max.heap.bytes", Long.toString(Runtime.getRuntime().maxMemory())); + properties.setProperty("client.count", "1"); + properties.setProperty("row.count", Integer.toString(ROW_COUNT)); + properties.setProperty("column.count", Integer.toString(COLUMN_COUNT)); + properties.setProperty("tablet.row.count", Integer.toString(TABLET_ROW_COUNT)); + properties.setProperty("warmup.round.count", Integer.toString(WARMUP_ROUND_COUNT)); + properties.setProperty("round.count", Integer.toString(ROUND_COUNT)); + properties.setProperty("column.types", join(dataTypes)); + properties.setProperty("numeric.row.requests.per.round", Integer.toString(ROW_COUNT)); + properties.setProperty( + "insert.tablet.requests.per.round", + Integer.toString((ROW_COUNT - 1) / TABLET_ROW_COUNT + 1)); + properties.setProperty("numeric.row.round.elapsed.nanos", join(result.numericRowElapsedNanos)); + properties.setProperty("insert.tablet.round.elapsed.nanos", join(result.tabletElapsedNanos)); + properties.setProperty( + "numeric.row.round.p50.nanos", Long.toString(result.numericRowP50Nanos())); + properties.setProperty( + "numeric.row.round.p95.nanos", Long.toString(result.numericRowP95Nanos())); + properties.setProperty("numeric.row.rows.per.second", format(result.numericRowRowsPerSecond())); + properties.setProperty( + "numeric.row.values.per.second", format(result.numericRowValuesPerSecond())); + properties.setProperty("insert.tablet.round.p50.nanos", Long.toString(result.tabletP50Nanos())); + properties.setProperty("insert.tablet.round.p95.nanos", Long.toString(result.tabletP95Nanos())); + properties.setProperty("insert.tablet.rows.per.second", format(result.tabletRowsPerSecond())); + properties.setProperty( + "insert.tablet.values.per.second", format(result.tabletValuesPerSecond())); + + final Path resultPath = + Paths.get( + System.getProperty( + RESULT_PATH_PROPERTY, "target/numeric-row-insert-performance.properties")); + final Path parent = resultPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + try (OutputStream outputStream = Files.newOutputStream(resultPath)) { + properties.store(outputStream, null); + } + } + + private static String join(List<TSDataType> values) { + final StringBuilder builder = new StringBuilder(); + for (int index = 0; index < values.size(); index++) { + if (index > 0) { + builder.append(','); + } + builder.append(values.get(index)); + } + return builder.toString(); + } + + private static String join(long[] values) { + final StringBuilder builder = new StringBuilder(); + for (int index = 0; index < values.length; index++) { + if (index > 0) { + builder.append(','); + } + builder.append(values[index]); + } + return builder.toString(); + } + + private static String format(double value) { + return String.format(Locale.ROOT, "%.2f", value); + } + + private static long percentile(long[] latencies, double percentile) { + final long[] sorted = latencies.clone(); + Arrays.sort(sorted); + return sorted[(int) Math.ceil(percentile * sorted.length) - 1]; + } + + private static final class NumericRowWorkload { + + private final String tableName; + private final NumericRow[] rows; + + private NumericRowWorkload(String tableName, NumericRow[] rows) { + this.tableName = tableName; + this.rows = rows; + } + } + + private static final class TabletWorkload { + + private final String tableName; + private final List<Tablet> tablets; + + private TabletWorkload(String tableName, List<Tablet> tablets) { + this.tableName = tableName; + this.tablets = tablets; + } + } + + private static final class BenchmarkResult { + + private final long[] numericRowElapsedNanos; + private final long[] tabletElapsedNanos; + + private BenchmarkResult(long[] numericRowElapsedNanos, long[] tabletElapsedNanos) { + this.numericRowElapsedNanos = numericRowElapsedNanos; + this.tabletElapsedNanos = tabletElapsedNanos; + } + + private long numericRowP50Nanos() { + return percentile(numericRowElapsedNanos, 0.5D); + } + + private long numericRowP95Nanos() { + return percentile(numericRowElapsedNanos, 0.95D); + } + + private double numericRowRowsPerSecond() { + return ROW_COUNT * 1_000_000_000.0D / numericRowP50Nanos(); + } + + private double numericRowValuesPerSecond() { + return numericRowRowsPerSecond() * COLUMN_COUNT; + } + + private long tabletP50Nanos() { + return percentile(tabletElapsedNanos, 0.5D); + } + + private long tabletP95Nanos() { + return percentile(tabletElapsedNanos, 0.95D); + } + + private double tabletRowsPerSecond() { + return ROW_COUNT * 1_000_000_000.0D / tabletP50Nanos(); + } + + private double tabletValuesPerSecond() { + return tabletRowsPerSecond() * COLUMN_COUNT; + } + } +} diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java index 48a5a994dd5..f9c7ca00444 100644 --- a/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/session/IoTDBTableModelSessionIT.java @@ -20,6 +20,7 @@ package org.apache.iotdb.relational.it.session; import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; import org.apache.iotdb.isession.SessionDataSet; import org.apache.iotdb.isession.pool.ITableSessionPool; import org.apache.iotdb.it.env.EnvFactory; @@ -66,7 +67,7 @@ public class IoTDBTableModelSessionIT { @Test public void testNumericRowInsert() throws Exception { - // Exercise both table-session entry points, schema reordering and missing STRING columns. + // Exercise direct and pooled table sessions, schema reordering and missing STRING columns. // The same values must remain readable from MemTable and after flushing to TsFile. try (ITableSession session = EnvFactory.getEnv().getTableSessionConnection()) { session.executeNonQueryStatement("CREATE DATABASE numeric_rows"); @@ -94,16 +95,26 @@ public class IoTDBTableModelSessionIT { 20260910, 1234 }; - session.insert("readings", 1, names, types, values); + session.insert( + new NumericRow( + "readings", + 1, + names, + types, + Collections.nCopies(types.size(), ColumnCategory.FIELD), + values, + new String[0])); try (ITableSessionPool pool = EnvFactory.getEnv().getTableSessionPool(1, "numeric_rows"); ITableSession pooledSession = pool.getSession()) { pooledSession.insert( - "readings", - 2, - names, - types, - Collections.nCopies(types.size(), ColumnCategory.FIELD), - values); + new NumericRow( + "readings", + 2, + names, + types, + Collections.nCopies(types.size(), ColumnCategory.FIELD), + values, + new String[0])); } assertNumericRows(session); session.executeNonQueryStatement("FLUSH"); @@ -127,28 +138,44 @@ public class IoTDBTableModelSessionIT { Arrays.asList( ColumnCategory.FIELD, ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.TAG); session.insert( - "readings", - 1, - names, - types, - categories, - new long[] {1, 10}, - new String[] {"设备甲😀", "华北"}); + new NumericRow( + "readings", + 1, + names, + types, + categories, + new long[] {1, 10}, + new String[] {"设备甲😀", "华北"})); try (ITableSessionPool pool = EnvFactory.getEnv().getTableSessionPool(1, "numeric_tag_rows"); ITableSession pooled = pool.getSession()) { pooled.insert( - "readings", - 1, - names, - types, - categories, - new long[] {0, 20}, - new String[] {"设备乙", "华北"}); + new NumericRow( + "readings", + 1, + names, + types, + categories, + new long[] {0, 20}, + new String[] {"设备乙", "华北"})); } session.insert( - "readings", 2, names, types, categories, new long[] {1, 30}, new String[] {"", null}); + new NumericRow( + "readings", + 2, + names, + types, + categories, + new long[] {1, 30}, + new String[] {"", null})); session.insert( - "readings", 2, names, types, categories, new long[] {0, 40}, new String[] {null, null}); + new NumericRow( + "readings", + 2, + names, + types, + categories, + new long[] {0, 40}, + new String[] {null, null})); assertNumericTagRows(session); session.executeNonQueryStatement("FLUSH"); assertNumericTagRows(session); diff --git a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/ITableSession.java b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/ITableSession.java index b5868e60ecd..af524f9a853 100644 --- a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/ITableSession.java +++ b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/ITableSession.java @@ -22,12 +22,8 @@ package org.apache.iotdb.isession; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; -import org.apache.tsfile.enums.ColumnCategory; -import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.write.record.Tablet; -import java.util.List; - /** * This interface defines a session for interacting with IoTDB tables. It supports operations such * as data insertion, executing queries, and closing the session. Implementations of this interface @@ -50,6 +46,18 @@ public interface ITableSession extends AutoCloseable { */ void insert(Tablet tablet) throws StatementExecutionException, IoTDBConnectionException; + /** + * Inserts one table-model {@link NumericRow} with numeric FIELDs and optional STRING TAGs. + * + * <p>This API requires a server supporting numeric row requests. The row's metadata and value + * arrays must not be modified until this call returns. + * + * @param row the numeric row to insert + * @throws StatementExecutionException if an error occurs while executing the statement + * @throws IoTDBConnectionException if there is an issue with the IoTDB connection + */ + void insert(NumericRow row) throws StatementExecutionException, IoTDBConnectionException; + /** * Executes a non-query SQL statement, such as a DDL or DML command. * diff --git a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRow.java b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRow.java new file mode 100644 index 00000000000..502c75e05e4 --- /dev/null +++ b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/NumericRow.java @@ -0,0 +1,97 @@ +/* + * 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.isession; + +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; + +import java.util.List; + +/** + * A table-model row with numeric FIELDs in {@code long[]} and STRING TAGs in {@code String[]}. + * + * <p>Column names, types and categories describe all supplied columns in the same order. The value + * arrays contain only their respective FIELD or TAG values, in occurrence order in the column list. + * TAGs may be null or empty; missing TAG columns are null. ATTRIBUTE columns, nonnumeric FIELDs and + * null FIELD values are not supported. + * + * <p>BOOLEAN uses zero for false and nonzero for true. INT32 and DATE use the low 32 bits (DATE is + * encoded as yyyyMMdd); INT64 and TIMESTAMP use the long value. FLOAT uses {@link + * Float#floatToRawIntBits(float)} in the low 32 bits and DOUBLE uses {@link + * Double#doubleToRawLongBits(double)}. + * + * <p>Metadata and arrays are retained without copying or boxing. Do not modify them while {@link + * ITableSession#insert(NumericRow)} is in progress. Arguments are validated when inserting the row. + */ +public final class NumericRow { + private final String tableName; + private final long timestamp; + private final List<String> columnNames; + private final List<TSDataType> dataTypes; + private final List<ColumnCategory> columnCategories; + private final long[] values; + private final String[] tagValues; + + /** Creates a row with numeric FIELDs and optional STRING TAGs in separate arrays. */ + public NumericRow( + String tableName, + long timestamp, + List<String> columnNames, + List<TSDataType> dataTypes, + List<ColumnCategory> columnCategories, + long[] values, + String[] tagValues) { + this.tableName = tableName; + this.timestamp = timestamp; + this.columnNames = columnNames; + this.dataTypes = dataTypes; + this.columnCategories = columnCategories; + this.values = values; + this.tagValues = tagValues; + } + + public String getTableName() { + return tableName; + } + + public long getTimestamp() { + return timestamp; + } + + public List<String> getColumnNames() { + return columnNames; + } + + public List<TSDataType> getDataTypes() { + return dataTypes; + } + + public List<ColumnCategory> getColumnCategories() { + return columnCategories; + } + + public long[] getValues() { + return values; + } + + public String[] getTagValues() { + return tagValues; + } +} diff --git a/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java b/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java index 69b87b2db16..3eb19971a92 100644 --- a/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java +++ b/iotdb-client/session/src/main/i18n/en/org/apache/iotdb/session/i18n/SessionMessages.java @@ -20,6 +20,8 @@ package org.apache.iotdb.session.i18n; public final class SessionMessages { + public static final String EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233 = "Numeric row arguments must be non-null; column metadata lengths and per-category value counts must match."; + public static final String EXCEPTION_NUMERIC_ROWS_SUPPORT_NUMERIC_FIELD_COLUMNS_AND_STRING_TAG_COLUMNS_ONLY_87159FFA = "Numeric rows support numeric FIELD columns and STRING TAG columns only."; // Session public static final String NODE_URLS_EMPTY = "nodeUrls shouldn't be empty."; @@ -286,4 +288,6 @@ public final class SessionMessages { public static final String EXCEPTION_RETRY_EXECUTE_STATEMENT_ARG_FAILED_ARG_TIMES_ARG_216C6873 = "retry to execute statement on %s failed %d times: %s"; public static final String EXCEPTION_SESSIONPOOL_DOESN_T_SUPPORT_EXECUTING_ARG_DIRECTLY_B778F701 = "SessionPool doesn't support executing %s directly"; + public static final String EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_AND_COLUMN_NAMES_TYPES_CATEGORIES_AND_VALUES_MUST_HAVE_EQUAL_LENGTHS_AB102B01 = "Numeric row arguments must be non-null and column names, types, categories and values must have equal lengths."; + public static final String EXCEPTION_NUMERIC_ROWS_SUPPORT_ONLY_FIELD_COLUMNS_DA7A8EE7 = "Numeric rows support only FIELD columns."; } diff --git a/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java b/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java index 2f28590ac88..22f3bea7821 100644 --- a/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java +++ b/iotdb-client/session/src/main/i18n/zh/org/apache/iotdb/session/i18n/SessionMessages.java @@ -20,6 +20,8 @@ package org.apache.iotdb.session.i18n; public final class SessionMessages { + public static final String EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233 = "数值行参数不能为 null;列元数据长度必须一致,FIELD 和 TAG 的数量必须分别与对应的值数组长度一致。"; + public static final String EXCEPTION_NUMERIC_ROWS_SUPPORT_NUMERIC_FIELD_COLUMNS_AND_STRING_TAG_COLUMNS_ONLY_87159FFA = "数值行仅支持数值类型的 FIELD 列和 STRING 类型的 TAG 列。"; // Session public static final String NODE_URLS_EMPTY = "nodeUrls 不能为空。"; @@ -284,4 +286,6 @@ public final class SessionMessages { public static final String EXCEPTION_RETRY_EXECUTE_STATEMENT_ARG_FAILED_ARG_TIMES_ARG_216C6873 = "在 %s 上重试执行语句失败 %d 次:%s"; public static final String EXCEPTION_SESSIONPOOL_DOESN_T_SUPPORT_EXECUTING_ARG_DIRECTLY_B778F701 = "SessionPool 不支持直接执行 %s"; + public static final String EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_AND_COLUMN_NAMES_TYPES_CATEGORIES_AND_VALUES_MUST_HAVE_EQUAL_LENGTHS_AB102B01 = "数值行参数不能为 null,列名、类型、类别和值的数量必须相同。"; + public static final String EXCEPTION_NUMERIC_ROWS_SUPPORT_ONLY_FIELD_COLUMNS_DA7A8EE7 = "数值行仅支持 FIELD 列。"; } diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java index 39bb9c06cba..716d14fa802 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java @@ -23,6 +23,7 @@ import org.apache.iotdb.common.rpc.thrift.TAggregationType; import org.apache.iotdb.common.rpc.thrift.TEndPoint; import org.apache.iotdb.isession.INodeSupplier; import org.apache.iotdb.isession.ISession; +import org.apache.iotdb.isession.NumericRow; import org.apache.iotdb.isession.SessionConfig; import org.apache.iotdb.isession.SessionDataSet; import org.apache.iotdb.isession.template.Template; @@ -66,6 +67,7 @@ import org.apache.iotdb.session.util.ThreadUtils; import org.apache.thrift.TException; import org.apache.tsfile.common.conf.TSFileDescriptor; +import org.apache.tsfile.enums.ColumnCategory; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.file.metadata.IDeviceID; import org.apache.tsfile.file.metadata.enums.CompressionType; @@ -2873,6 +2875,55 @@ public class Session implements ISession { } } + /** Inserts numeric FIELDs and STRING TAGs without constructing a boxed row. */ + public void insertRelationalRecord(NumericRow row) + throws IoTDBConnectionException, StatementExecutionException { + if (row == null || row.getTableName() == null) { + throw new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233); + } + String tableName = row.getTableName(); + long timestamp = row.getTimestamp(); + List<String> measurements = row.getColumnNames(); + List<TSDataType> types = row.getDataTypes(); + List<ColumnCategory> columnCategories = row.getColumnCategories(); + long[] values = row.getValues(); + String[] tagValues = row.getTagValues(); + ByteBuffer valueBuffer = + SessionUtils.getValueBuffer(types, values, measurements, columnCategories, tagValues); + TSInsertRecordReq request = new TSInsertRecordReq(); + request.setPrefixPath(tableName); + request.setTimestamp(timestamp); + request.setMeasurements(measurements); + request.setValues(valueBuffer); + request.setIsWriteToTable(true); + request.setIsNumericRow(true); + request.setIsAligned(true); + request.setColumnCategoryies( + columnCategories.stream() + .map(category -> (byte) category.ordinal()) + .collect(Collectors.toList())); + String[] deviceSegments = new String[tagValues.length + 1]; + deviceSegments[0] = tableName; + System.arraycopy(tagValues, 0, deviceSegments, 1, tagValues.length); + IDeviceID deviceId = IDeviceID.Factory.DEFAULT_FACTORY.create(deviceSegments); + try { + getSessionConnection(deviceId).insertRecord(request); + } catch (RedirectException e) { + handleRedirection(deviceId, e.getEndPoint()); + } catch (IoTDBConnectionException e) { + if (!enableRedirection || tableModelDeviceIdToEndpoint.remove(deviceId) == null) { + throw e; + } + try { + getDefaultSessionConnection().insertRecord(request); + } catch (RedirectException redirect) { + handleRedirection(deviceId, redirect.getEndPoint()); + } + } + } + private void insertRelationalTabletWithLeaderCache(Tablet tablet) throws IoTDBConnectionException, StatementExecutionException { Map<SessionConnection, Tablet> relationalTabletGroup = new HashMap<>(); diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java index 059fdea3469..38cc6d72cea 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSession.java @@ -20,6 +20,7 @@ package org.apache.iotdb.session; import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; import org.apache.iotdb.isession.SessionDataSet; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; @@ -39,6 +40,11 @@ public class TableSession implements ITableSession { session.insertRelationalTablet(tablet); } + @Override + public void insert(NumericRow row) throws StatementExecutionException, IoTDBConnectionException { + session.insertRelationalRecord(row); + } + @Override public void executeNonQueryStatement(String sql) throws IoTDBConnectionException, StatementExecutionException { diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java index 4cf7ca980f3..8f158f4ab4f 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionWrapper.java @@ -20,6 +20,7 @@ package org.apache.iotdb.session.pool; import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; import org.apache.iotdb.isession.SessionDataSet; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; @@ -68,6 +69,18 @@ public class TableSessionWrapper implements ITableSession { } } + @Override + public void insert(NumericRow row) throws StatementExecutionException, IoTDBConnectionException { + try { + session.insertRelationalRecord(row); + } catch (IoTDBConnectionException e) { + sessionPool.cleanSessionAndMayThrowConnectionException(session); + closed.set(true); + session = null; + throw e; + } + } + @Override public SessionDataSet executeQueryStatement(String sql) throws StatementExecutionException, IoTDBConnectionException { diff --git a/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java b/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java index b3b29f0be93..0d838b69254 100644 --- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java +++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java @@ -163,6 +163,80 @@ public class SessionUtils { return buffer; } + /** Encodes separate, densely packed FIELD and TAG arrays in the metadata column order. */ + public static ByteBuffer getValueBuffer( + List<TSDataType> types, + long[] values, + List<String> measurements, + List<ColumnCategory> categories, + String[] tagValues) + throws IoTDBConnectionException { + if (types == null + || values == null + || measurements == null + || categories == null + || tagValues == null + || types.size() != measurements.size() + || types.size() != categories.size()) { + throw invalidNumericRowArguments(); + } + int fieldCount = 0; + int tagCount = 0; + int length = types.size(); + for (int i = 0; i < types.size(); i++) { + TSDataType type = types.get(i); + if (categories.get(i) == ColumnCategory.TAG && type == TSDataType.STRING) { + if (tagCount >= tagValues.length) { + throw invalidNumericRowArguments(); + } + length += ReadWriteIOUtils.sizeToWrite(tagValues[tagCount++]); + } else if (categories.get(i) == ColumnCategory.FIELD && type != null) { + fieldCount++; + length += + switch (type) { + case BOOLEAN -> Byte.BYTES; + case INT32, DATE, FLOAT -> Integer.BYTES; + case INT64, TIMESTAMP, DOUBLE -> Long.BYTES; + default -> throw new IoTDBConnectionException(MSG_UNSUPPORTED_DATA_TYPE + type); + }; + } else { + throw new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROWS_SUPPORT_NUMERIC_FIELD_COLUMNS_AND_STRING_TAG_COLUMNS_ONLY_87159FFA); + } + } + if (fieldCount != values.length || tagCount != tagValues.length) { + throw invalidNumericRowArguments(); + } + ByteBuffer buffer = ByteBuffer.allocate(length); + int fieldIndex = 0; + int tagIndex = 0; + for (int i = 0; i < types.size(); i++) { + TSDataType type = types.get(i); + ReadWriteIOUtils.write(type, buffer); + if (categories.get(i) == ColumnCategory.TAG) { + // A length of -1 represents a null TAG; an empty TAG has length zero. + ReadWriteIOUtils.write(tagValues[tagIndex++], buffer); + } else { + long value = values[fieldIndex++]; + switch (type) { + case BOOLEAN -> buffer.put((byte) (value == 0 ? 0 : 1)); + case INT32, DATE, FLOAT -> buffer.putInt((int) value); + case INT64, TIMESTAMP, DOUBLE -> buffer.putLong(value); + default -> throw new IoTDBConnectionException(MSG_UNSUPPORTED_DATA_TYPE + type); + } + } + } + buffer.flip(); + return buffer; + } + + private static IllegalArgumentException invalidNumericRowArguments() { + return new IllegalArgumentException( + SessionMessages + .EXCEPTION_NUMERIC_ROW_ARGUMENTS_MUST_BE_NON_NULL_COLUMN_METADATA_LENGTHS_AND_PER_CATEGORY_VALUE_COUNTS_MUST_MATCH_EB12B233); + } + public static int calculateLength(List<TSDataType> types, List<? extends Object> values) throws IoTDBConnectionException { int res = 0; diff --git a/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionTest.java b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionTest.java index e4d2dbbb41d..88280ac8b4b 100644 --- a/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionTest.java +++ b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionTest.java @@ -22,13 +22,18 @@ package org.apache.iotdb.session; import org.apache.iotdb.common.rpc.thrift.TAggregationType; import org.apache.iotdb.common.rpc.thrift.TEndPoint; import org.apache.iotdb.isession.ISession; +import org.apache.iotdb.isession.ITableSession; +import org.apache.iotdb.isession.NumericRow; import org.apache.iotdb.isession.SessionConfig; import org.apache.iotdb.isession.util.Version; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; +import org.apache.iotdb.service.rpc.thrift.TSInsertRecordReq; import org.apache.iotdb.service.rpc.thrift.TSQueryTemplateResp; +import org.apache.iotdb.session.util.SessionUtils; import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.enums.ColumnCategory; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.file.metadata.enums.CompressionType; import org.apache.tsfile.file.metadata.enums.TSEncoding; @@ -41,6 +46,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -134,6 +140,80 @@ public class SessionTest { .build(); } + @Test + public void testNumericTableRowRequest() throws Exception { + // A BOOLEAN/DOUBLE row must carry the table RPC flags and explicit FIELD categories. + ITableSession tableSession = new TableSession((Session) session); + List<String> names = Arrays.asList("enabled", "temperature"); + List<TSDataType> types = Arrays.asList(TSDataType.BOOLEAN, TSDataType.DOUBLE); + tableSession.insert( + new NumericRow( + "readings", + 123L, + names, + types, + Collections.nCopies(types.size(), ColumnCategory.FIELD), + new long[] {1L, Double.doubleToRawLongBits(-0.0)}, + new String[0])); + ArgumentCaptor<TSInsertRecordReq> captor = ArgumentCaptor.forClass(TSInsertRecordReq.class); + Mockito.verify(sessionConnection).insertRecord(captor.capture()); + TSInsertRecordReq request = captor.getValue(); + Assert.assertTrue(request.isIsNumericRow()); + Assert.assertTrue(request.isIsWriteToTable()); + Assert.assertTrue(request.isIsAligned()); + assertEquals("readings", request.getPrefixPath()); + assertEquals(123L, request.getTimestamp()); + assertEquals(names, request.getMeasurements()); + assertEquals( + Collections.nCopies(2, (byte) ColumnCategory.FIELD.ordinal()), + request.getColumnCategoryies()); + } + + @Test + public void testNumericTableRowRejectsNonFieldColumnsBeforeRpc() throws Exception { + // Numeric TAG values are invalid and must fail locally without sending a request. + ITableSession tableSession = new TableSession((Session) session); + Assert.assertThrows( + IllegalArgumentException.class, () -> tableSession.insert((NumericRow) null)); + Assert.assertThrows( + IllegalArgumentException.class, + () -> + tableSession.insert( + new NumericRow( + "readings", + 1L, + Collections.singletonList("tag"), + Collections.singletonList(TSDataType.INT64), + Collections.singletonList(ColumnCategory.TAG), + new long[] {1}, + new String[0]))); + Mockito.verify(sessionConnection, Mockito.never()).insertRecord(any(TSInsertRecordReq.class)); + } + + @Test + public void testNumericTableRowWithTagsRequest() throws Exception { + // The NumericRow entry point must forward the TAG payload and metadata with numeric flags. + ITableSession tableSession = new TableSession((Session) session); + List<String> names = Arrays.asList("tag", "enabled"); + List<TSDataType> types = Arrays.asList(TSDataType.STRING, TSDataType.BOOLEAN); + List<ColumnCategory> categories = Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD); + tableSession.insert( + new NumericRow( + "readings", 123L, names, types, categories, new long[] {1}, new String[] {"device"})); + ArgumentCaptor<TSInsertRecordReq> captor = ArgumentCaptor.forClass(TSInsertRecordReq.class); + Mockito.verify(sessionConnection).insertRecord(captor.capture()); + TSInsertRecordReq request = captor.getValue(); + Assert.assertTrue(request.isIsNumericRow()); + Assert.assertTrue(request.isIsWriteToTable()); + assertEquals( + Arrays.asList((byte) ColumnCategory.TAG.ordinal(), (byte) ColumnCategory.FIELD.ordinal()), + request.getColumnCategoryies()); + assertEquals( + SessionUtils.getValueBuffer( + types, new long[] {1}, names, categories, new String[] {"device"}), + request.bufferForValues()); + } + @Test public void testTimeZone() throws IoTDBConnectionException, StatementExecutionException { String timeZone = session.getTimeZone(); diff --git a/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java b/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java index b8524d4b311..d2c24a4da8e 100644 --- a/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java +++ b/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java @@ -28,6 +28,7 @@ import org.apache.tsfile.file.metadata.enums.CompressionType; import org.apache.tsfile.file.metadata.enums.TSEncoding; import org.apache.tsfile.utils.Binary; import org.apache.tsfile.utils.BitMap; +import org.apache.tsfile.utils.ReadWriteIOUtils; import org.apache.tsfile.write.record.Tablet; import org.apache.tsfile.write.schema.IMeasurementSchema; import org.apache.tsfile.write.schema.MeasurementSchema; @@ -45,6 +46,156 @@ import static org.junit.Assert.assertThrows; public class SessionUtilsTest { + @Test + public void testNumericRowEncodingPreservesRawBits() throws IoTDBConnectionException { + List<TSDataType> types = + Arrays.asList( + TSDataType.BOOLEAN, + TSDataType.BOOLEAN, + TSDataType.INT32, + TSDataType.INT64, + TSDataType.FLOAT, + TSDataType.DOUBLE, + TSDataType.DATE, + TSDataType.TIMESTAMP); + // Noncanonical NaN payloads must survive encoding without a float/double round trip. + long[] values = { + 0, + -7, + Integer.MIN_VALUE, + Long.MIN_VALUE, + 0x7fc01234, + 0x7ff8000000001234L, + 20260910, + Long.MAX_VALUE + }; + ByteBuffer actual = + SessionUtils.getValueBuffer( + types, + values, + Collections.nCopies(8, "s"), + Collections.nCopies(8, ColumnCategory.FIELD), + new String[0]); + ByteBuffer expected = ByteBuffer.allocate(46); + expected.put(TSDataType.BOOLEAN.serialize()).put((byte) 0); + expected.put(TSDataType.BOOLEAN.serialize()).put((byte) 1); + expected.put(TSDataType.INT32.serialize()).putInt(Integer.MIN_VALUE); + expected.put(TSDataType.INT64.serialize()).putLong(Long.MIN_VALUE); + expected.put(TSDataType.FLOAT.serialize()).putInt(0x7fc01234); + expected.put(TSDataType.DOUBLE.serialize()).putLong(0x7ff8000000001234L); + expected.put(TSDataType.DATE.serialize()).putInt(20260910); + expected.put(TSDataType.TIMESTAMP.serialize()).putLong(Long.MAX_VALUE); + expected.flip(); + Assert.assertEquals(expected, actual); + } + + @Test + public void testNumericRowRejectsInvalidValues() { + // Reject mismatched lengths, null types and every unsupported type before encoding. + assertThrows( + IllegalArgumentException.class, + () -> + SessionUtils.getValueBuffer( + Collections.singletonList(TSDataType.INT64), + new long[0], + Collections.singletonList("s"), + Collections.singletonList(ColumnCategory.FIELD), + new String[0])); + assertThrows( + IllegalArgumentException.class, + () -> + SessionUtils.getValueBuffer( + Collections.singletonList(null), + new long[] {1}, + Collections.singletonList("s"), + Collections.singletonList(ColumnCategory.FIELD), + new String[0])); + for (TSDataType type : + Arrays.asList( + TSDataType.TEXT, + TSDataType.STRING, + TSDataType.BLOB, + TSDataType.OBJECT, + TSDataType.VECTOR, + TSDataType.UNKNOWN)) { + assertThrows( + IoTDBConnectionException.class, + () -> + SessionUtils.getValueBuffer( + Collections.singletonList(type), + new long[] {1}, + Collections.singletonList("s"), + Collections.singletonList(ColumnCategory.FIELD), + new String[0])); + } + } + + @Test + public void testNumericRowWithSeparateTags() throws Exception { + // TAG strings, including null and empty, must not consume slots from the compact long[]. + List<TSDataType> types = + Arrays.asList( + TSDataType.STRING, + TSDataType.DOUBLE, + TSDataType.STRING, + TSDataType.BOOLEAN, + TSDataType.STRING); + List<ColumnCategory> categories = + Arrays.asList( + ColumnCategory.TAG, + ColumnCategory.FIELD, + ColumnCategory.TAG, + ColumnCategory.FIELD, + ColumnCategory.TAG); + ByteBuffer buffer = + SessionUtils.getValueBuffer( + types, + new long[] {0x7ff8000000001234L, -1}, + Arrays.asList("a", "d", "b", "enabled", "c"), + categories, + new String[] {"设备😀", null, ""}); + Assert.assertEquals(TSDataType.STRING.serialize(), buffer.get()); + Assert.assertEquals("设备😀", ReadWriteIOUtils.readString(buffer)); + Assert.assertEquals(TSDataType.DOUBLE.serialize(), buffer.get()); + Assert.assertEquals(0x7ff8000000001234L, buffer.getLong()); + Assert.assertEquals(TSDataType.STRING.serialize(), buffer.get()); + Assert.assertEquals(-1, buffer.getInt()); + Assert.assertEquals(TSDataType.BOOLEAN.serialize(), buffer.get()); + Assert.assertEquals(1, buffer.get()); + Assert.assertEquals(TSDataType.STRING.serialize(), buffer.get()); + Assert.assertEquals(0, buffer.getInt()); + Assert.assertFalse(buffer.hasRemaining()); + } + + @Test + public void testNumericRowRejectsInvalidTagArrays() { + // Mismatched per-category counts and ATTRIBUTE values must fail before sending the RPC. + List<TSDataType> types = Arrays.asList(TSDataType.STRING, TSDataType.INT64); + List<String> names = Arrays.asList("tag", "field"); + List<ColumnCategory> categories = Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD); + for (String[] tags : Arrays.asList(new String[0], new String[] {"a", "b"})) { + assertThrows( + IllegalArgumentException.class, + () -> SessionUtils.getValueBuffer(types, new long[] {1}, names, categories, tags)); + } + assertThrows( + IllegalArgumentException.class, + () -> + SessionUtils.getValueBuffer(types, new long[0], names, categories, new String[] {"a"})); + assertThrows( + IllegalArgumentException.class, + () -> SessionUtils.getValueBuffer(types, new long[] {1}, names, categories, null)); + assertThrows( + IllegalArgumentException.class, + () -> + SessionUtils.getValueBuffer( + types, + new long[] {1}, + names, + Arrays.asList(ColumnCategory.ATTRIBUTE, ColumnCategory.FIELD), + new String[] {"a"})); + } + @Test public void testGetTimeBuffer() { List<IMeasurementSchema> schemas = new ArrayList<>(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeInsertNodeTabletInsertionEvent.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeInsertNodeTabletInsertionEvent.java index 3bebbc91153..dc8a8877b6d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeInsertNodeTabletInsertionEvent.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/PipeInsertNodeTabletInsertionEvent.java @@ -46,10 +46,12 @@ import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager; import org.apache.iotdb.db.pipe.resource.memory.InsertNodeMemoryEstimator; import org.apache.iotdb.db.pipe.resource.memory.PipeMemoryWeightUtil; import org.apache.iotdb.db.pipe.resource.memory.PipeTabletMemoryBlock; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; @@ -366,8 +368,8 @@ public class PipeInsertNodeTabletInsertionEvent extends PipeInsertionEvent return true; } - if (insertNode instanceof InsertRowNode) { - final long timestamp = ((InsertRowNode) insertNode).getTime(); + if (insertNode instanceof AbstractInsertRowNode row) { + final long timestamp = row.getTime(); return startTime <= timestamp && timestamp <= endTime; } @@ -409,13 +411,14 @@ public class PipeInsertNodeTabletInsertionEvent extends PipeInsertionEvent try { final InsertNode insertNode = getInsertNode(); - if (insertNode instanceof RelationalInsertRowNode + if (insertNode instanceof RelationalInsertNumericRowNode + || insertNode instanceof RelationalInsertRowNode || insertNode instanceof RelationalInsertTabletNode || insertNode instanceof RelationalInsertRowsNode) { return true; } - if (insertNode instanceof InsertRowNode || insertNode instanceof InsertTabletNode) { + if (insertNode instanceof AbstractInsertRowNode || insertNode instanceof InsertTabletNode) { final PartialPath devicePartialPath = insertNode.getTargetPath(); return Objects.isNull(devicePartialPath) || treePattern.mayOverlapWithDevice(devicePartialPath.getIDeviceIDAsFullDevice()); @@ -539,6 +542,7 @@ public class PipeInsertNodeTabletInsertionEvent extends PipeInsertionEvent pipeTaskMeta, this, insertRowNode, treePattern, userEntity)); } break; + case RELATIONAL_INSERT_NUMERIC_ROW: case RELATIONAL_INSERT_ROW: case RELATIONAL_INSERT_TABLET: eventParsers.add( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventParser.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventParser.java index 545a587aec4..886b070a84d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventParser.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventParser.java @@ -25,7 +25,7 @@ import org.apache.iotdb.commons.pipe.event.EnrichedEvent; import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.event.common.tablet.PipeTabletUtils; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.pipe.api.access.Row; import org.apache.iotdb.pipe.api.collector.RowCollector; @@ -115,7 +115,7 @@ public abstract class TabletInsertionEventParser { //////////////////////////// parse //////////////////////////// - protected void parse(final InsertRowNode insertRowNode) { + protected void parse(final AbstractInsertRowNode insertRowNode) { final String[] originColumnNameStringList = insertRowNode.getMeasurements(); final int originColumnSize = originColumnNameStringList == null ? 0 : originColumnNameStringList.length; @@ -133,7 +133,8 @@ public abstract class TabletInsertionEventParser { final MeasurementSchema[] originMeasurementSchemaList = insertRowNode.getMeasurementSchemas(); final TsTableColumnCategory[] originColumnCategories = insertRowNode.getColumnCategories(); final TSDataType[] originValueDataTypes = insertRowNode.getDataTypes(); - final Object[] originValues = insertRowNode.getValues(); + // Read individual columns so numeric rows never need an Object[] row snapshot. + final int originValueCount = insertRowNode.getValueCount(); generateColumnIndexMapper( originColumnNameStringList, originColumnIndex2FilteredColumnIndexMapperList); @@ -147,8 +148,7 @@ public abstract class TabletInsertionEventParser { originMeasurementSchemaList, originValueDataTypes, i) - && originValues != null - && i < originValues.length); + && i < originValueCount); this.measurementSchemaList = new MeasurementSchema[filteredColumnSize]; this.columnNameStringList = new String[filteredColumnSize]; @@ -170,7 +170,7 @@ public abstract class TabletInsertionEventParser { : ColumnCategory.FIELD; this.valueColumnDataTypes[filteredColumnIndex] = originValueDataTypes[i]; final BitMap bitMap = new BitMap(this.timestampColumn.length); - if (Objects.isNull(originValues[i]) || Objects.isNull(originValueDataTypes[i])) { + if (insertRowNode.isNull(i) || Objects.isNull(originValueDataTypes[i])) { fillNullValue( originValueDataTypes[i], this.valueColumns, @@ -180,7 +180,12 @@ public abstract class TabletInsertionEventParser { } else { this.valueColumns[filteredColumnIndex] = filterValueColumnsByRowIndexList( - originValueDataTypes[i], originValues[i], rowIndexList, true, null, bitMap); + originValueDataTypes[i], + insertRowNode.getValue(i), + rowIndexList, + true, + null, + bitMap); } this.nullValueColumnBitmaps[filteredColumnIndex] = bitMap; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventTablePatternParser.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventTablePatternParser.java index 14708aa4be9..84dfb70bacc 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventTablePatternParser.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/event/common/tablet/parser/TabletInsertionEventTablePatternParser.java @@ -26,6 +26,7 @@ import org.apache.iotdb.commons.pipe.event.EnrichedEvent; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.event.common.tablet.PipeTabletUtils; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; import org.apache.iotdb.pipe.api.access.Row; @@ -60,7 +61,9 @@ public class TabletInsertionEventTablePatternParser extends TabletInsertionEvent super(pipeTaskMeta, sourceEvent); this.pattern = pattern; - if (insertNode instanceof RelationalInsertRowNode) { + if (insertNode instanceof RelationalInsertNumericRowNode numeric) { + parse(numeric); + } else if (insertNode instanceof RelationalInsertRowNode) { parse((RelationalInsertRowNode) insertNode); } else if (insertNode instanceof RelationalInsertTabletNode) { parse((RelationalInsertTabletNode) insertNode); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipePlanToStatementVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipePlanToStatementVisitor.java index 6158c19bb99..210defbeb70 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipePlanToStatementVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/visitor/PipePlanToStatementVisitor.java @@ -42,6 +42,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNod import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.CreateOrUpdateTableDeviceNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableDeviceAttributeUpdateNode; @@ -50,6 +51,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Delete; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Update; import org.apache.iotdb.db.queryengine.plan.statement.Statement; import org.apache.iotdb.db.queryengine.plan.statement.crud.DeleteDataStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement; @@ -84,6 +86,19 @@ public class PipePlanToStatementVisitor implements PlanVisitor<Object, Void> { node)); } + @Override + public InsertNumericRowStatement visitRelationalInsertNumericRow( + final RelationalInsertNumericRowNode node, final Void context) { + InsertNumericRowStatement statement = new InsertNumericRowStatement(); + statement.setDevicePath(node.getTargetPath()); + statement.setTime(node.getTime()); + statement.setMeasurements(node.getMeasurements()); + statement.setDataTypes(node.getDataTypes()); + statement.setNumericValues(node.getNumericValues()); + statement.setColumnCategories(node.getColumnCategories()); + return statement; + } + @Override public InsertRowStatement visitInsertRow(final InsertRowNode node, final Void context) { // MeasurementSchema is only allowed to be set at the analysis type stage. However, InsertNode diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBatchReqV2.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBatchReqV2.java index 2cec219fa75..e8b765366c1 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBatchReqV2.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBatchReqV2.java @@ -28,6 +28,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertMultiTabletsStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement; @@ -78,9 +79,12 @@ public class PipeTransferTabletBatchReqV2 extends TPipeTransferReq { continue; } if (statement.isWriteToTable()) { - if (statement instanceof InsertRowStatement) { - addTableModelInsertRowStatement( - tableModelDatabaseInsertRowStatementMap, (InsertRowStatement) statement); + if (statement instanceof InsertNumericRowStatement) { + // Keep numeric rows independent: grouping into InsertRowsStatement would box their + // values. + statements.add(statement); + } else if (statement instanceof InsertRowStatement row) { + addTableModelInsertRowStatement(tableModelDatabaseInsertRowStatementMap, row); } else if (statement instanceof InsertTabletStatement) { statements.add(statement); } else if (statement instanceof InsertRowsStatement) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReq.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReq.java index cf4a746cb0e..cf89d64461a 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReq.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReq.java @@ -24,8 +24,8 @@ import org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeRequestType import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiver; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; @@ -55,7 +55,7 @@ public class PipeTransferTabletBinaryReq extends TPipeTransferReq { public InsertBaseStatement constructStatement() { final InsertNode insertNode = parseByteBuffer(); - if (!(insertNode instanceof InsertRowNode + if (!(insertNode instanceof AbstractInsertRowNode || insertNode instanceof InsertTabletNode || insertNode instanceof InsertRowsNode)) { throw new UnsupportedOperationException( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReqV2.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReqV2.java index 196af0b16d2..d764dc7ce98 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReqV2.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletBinaryReqV2.java @@ -24,8 +24,8 @@ import org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeRequestType import org.apache.iotdb.commons.utils.PathUtils; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiver; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; @@ -56,7 +56,7 @@ public class PipeTransferTabletBinaryReqV2 extends PipeTransferTabletBinaryReq { public InsertBaseStatement constructStatement() { final InsertNode insertNode = parseByteBuffer(); - if (!(insertNode instanceof InsertRowNode + if (!(insertNode instanceof AbstractInsertRowNode || insertNode instanceof InsertTabletNode || insertNode instanceof InsertRowsNode)) { throw new UnsupportedOperationException( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReq.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReq.java index 42f353c2506..40fd29ad98f 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReq.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReq.java @@ -24,8 +24,8 @@ import org.apache.iotdb.commons.pipe.sink.payload.thrift.request.PipeRequestType import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiver; import org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; @@ -51,7 +51,7 @@ public class PipeTransferTabletInsertNodeReq extends TPipeTransferReq { } public InsertBaseStatement constructStatement() { - if (!(insertNode instanceof InsertRowNode + if (!(insertNode instanceof AbstractInsertRowNode || insertNode instanceof InsertTabletNode || insertNode instanceof InsertRowsNode)) { throw new UnsupportedOperationException( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReqV2.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReqV2.java index 4fc18398dda..45dd2607ef6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReqV2.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/payload/evolvable/request/PipeTransferTabletInsertNodeReqV2.java @@ -25,8 +25,8 @@ import org.apache.iotdb.commons.utils.PathUtils; import org.apache.iotdb.db.i18n.DataNodePipeMessages; import org.apache.iotdb.db.pipe.receiver.protocol.thrift.IoTDBDataNodeReceiver; import org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; @@ -56,7 +56,7 @@ public class PipeTransferTabletInsertNodeReqV2 extends PipeTransferTabletInsertN @Override public InsertBaseStatement constructStatement() { - if (!(insertNode instanceof InsertRowNode + if (!(insertNode instanceof AbstractInsertRowNode || insertNode instanceof InsertTabletNode || insertNode instanceof InsertRowsNode)) { throw new UnsupportedOperationException( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java index 6fc521a7f72..a49fd33161c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/executor/RegionWriteExecutor.java @@ -71,6 +71,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsOf import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.CreateOrUpdateTableDeviceNode; import org.apache.iotdb.db.queryengine.plan.relational.planner.node.schema.TableNodeLocationAddNode; @@ -231,6 +232,12 @@ public class RegionWriteExecutor { } } + @Override + public RegionExecutionResult visitRelationalInsertNumericRow( + final RelationalInsertNumericRowNode node, final WritePlanNodeExecutionContext context) { + return executeDataInsert(node, context); + } + @Override public RegionExecutionResult visitInsertRow( final InsertRowNode node, final WritePlanNodeExecutionContext context) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGenerator.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGenerator.java index 7464e6d99fb..0303eed2777 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGenerator.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGenerator.java @@ -47,7 +47,9 @@ import org.apache.iotdb.db.queryengine.plan.statement.component.ResultColumn; import org.apache.iotdb.db.queryengine.plan.statement.component.SelectComponent; import org.apache.iotdb.db.queryengine.plan.statement.component.WhereCondition; import org.apache.iotdb.db.queryengine.plan.statement.crud.DeleteDataStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertMultiTabletsStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsOfOneDeviceStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; @@ -70,6 +72,7 @@ import org.apache.iotdb.db.queryengine.plan.statement.metadata.template.ShowSche import org.apache.iotdb.db.queryengine.plan.statement.metadata.template.UnsetSchemaTemplateStatement; import org.apache.iotdb.db.schemaengine.schemaregion.utils.MetaFormatUtils; import org.apache.iotdb.db.schemaengine.template.TemplateQueryType; +import org.apache.iotdb.db.utils.NumericRowValues; import org.apache.iotdb.db.utils.QueryDataSetUtils; import org.apache.iotdb.db.utils.TabletDecoder; import org.apache.iotdb.mpp.rpc.thrift.TFetchTimeseriesReq; @@ -278,19 +281,38 @@ public class StatementGenerator { return queryStatement; } - public static InsertRowStatement createStatement(TSInsertRecordReq insertRecordReq) + public static InsertBaseStatement createStatement(TSInsertRecordReq insertRecordReq) throws IllegalPathException, QueryProcessException { final long startTime = System.nanoTime(); // construct insert statement - InsertRowStatement insertStatement = new InsertRowStatement(); + InsertBaseStatement insertStatement = + insertRecordReq.isIsNumericRow() + ? new InsertNumericRowStatement() + : new InsertRowStatement(); insertStatement.setDevicePath( - DEVICE_PATH_CACHE.getPartialPath(insertRecordReq.getPrefixPath())); + insertRecordReq.isIsWriteToTable() + ? new PartialPath(insertRecordReq.getPrefixPath(), false) + : DEVICE_PATH_CACHE.getPartialPath(insertRecordReq.getPrefixPath())); TimestampPrecisionUtils.checkTimestampPrecision(insertRecordReq.getTimestamp()); - insertStatement.setTime(insertRecordReq.getTimestamp()); + insertStatement.setMeasurements(insertRecordReq.getMeasurements().toArray(new String[0])); insertStatement.setAligned(insertRecordReq.isAligned); - insertStatement.fillValues(insertRecordReq.values); - if (insertRecordReq.isSetIsWriteToTable()) { + if (insertRecordReq.isIsNumericRow()) { + if (!insertRecordReq.isIsWriteToTable()) { + throw new IllegalArgumentException( + DataNodeQueryMessages + .QUERY_EXCEPTION_MISSING_OR_INVALID_COLUMN_CATEGORIES_FOR_TABLE_INSERTION_5DF990B9); + } + InsertNumericRowStatement numeric = (InsertNumericRowStatement) insertStatement; + numeric.setTime(insertRecordReq.getTimestamp()); + numeric.fillValues(insertRecordReq.values); + insertStatement.setAligned(true); + } else { + InsertRowStatement row = (InsertRowStatement) insertStatement; + row.setTime(insertRecordReq.getTimestamp()); + row.fillValues(insertRecordReq.values); + } + if (insertRecordReq.isIsWriteToTable()) { insertStatement.setWriteToTable(insertRecordReq.isIsWriteToTable()); if (!insertRecordReq.isSetColumnCategoryies() || insertRecordReq.getColumnCategoryiesSize() != insertRecordReq.getMeasurementsSize()) { @@ -305,6 +327,18 @@ public class StatementGenerator { TsTableColumnCategory.fromTsFileColumnCategory( ColumnCategory.values()[insertRecordReq.getColumnCategoryies().get(i).intValue()]); } + if (insertRecordReq.isIsNumericRow()) { + for (int i = 0; i < columnCategories.length; i++) { + TSDataType type = insertStatement.getDataType(i); + if (!(columnCategories[i] == TsTableColumnCategory.TAG && type == TSDataType.STRING) + && !(columnCategories[i] == TsTableColumnCategory.FIELD + && NumericRowValues.supports(type))) { + throw new IllegalArgumentException( + DataNodeQueryMessages + .QUERY_EXCEPTION_MISSING_OR_INVALID_COLUMN_CATEGORIES_FOR_TABLE_INSERTION_5DF990B9); + } + } + } insertStatement.setColumnCategories(columnCategories); } PERFORMANCE_OVERVIEW_METRICS.recordParseCost(System.nanoTime() - startTime); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java index a5b886ff4b0..864c7401c9c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/PlanVisitor.java @@ -117,6 +117,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsOf import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; @@ -581,6 +582,10 @@ public interface PlanVisitor<R, C> extends ICoreQueryPlanVisitor<R, C> { return visitPlan(node, context); } + default R visitRelationalInsertNumericRow(RelationalInsertNumericRowNode node, C context) { + return visitPlan(node, context); + } + default R visitRelationalInsertRow(RelationalInsertRowNode node, C context) { return visitInsertRow(node, context); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/AbstractInsertRowNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/AbstractInsertRowNode.java new file mode 100644 index 00000000000..f1c0785afa3 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/write/AbstractInsertRowNode.java @@ -0,0 +1,64 @@ +/* + * 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.queryengine.plan.planner.plan.node.write; + +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId; +import org.apache.iotdb.db.queryengine.plan.relational.metadata.fetcher.cache.LastCacheUpdateSource; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.IWALByteBufferView; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryValue; + +import org.apache.tsfile.enums.TSDataType; + +/** + * Storage-facing contract for single-row writes. Concrete nodes own their value representation and + * wire format; sharing this contract never converts a primitive row to an object row. + */ +public abstract class AbstractInsertRowNode extends InsertNode + implements WALEntryValue, LastCacheUpdateSource { + protected AbstractInsertRowNode(PlanNodeId id) { + super(id); + } + + protected AbstractInsertRowNode( + PlanNodeId id, + PartialPath path, + boolean aligned, + String[] measurements, + TSDataType[] dataTypes) { + super(id, path, aligned, measurements, dataTypes); + } + + public abstract long getTime(); + + public abstract int getValueCount(); + + public abstract boolean isNull(int index); + + /** Boxes only the requested cell for cache and Tablet consumers. */ + public abstract Object getValue(int index); + + /** Returns the original backing storage for allocation accounting, without copying it. */ + public abstract Object getValuesForMemoryAccounting(); + + public abstract void updateLastCache(String databaseName); + + public abstract void serializeToWAL(IWALByteBufferView buffer, long encodedSearchIndex); +} 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 777e0de391a..20bef3dc8ee 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 @@ -185,6 +185,7 @@ import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Explain; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.ExplainAnalyze; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.FetchDevice; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Insert; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertNumericRow; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertRow; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertRows; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertTablet; @@ -1223,6 +1224,11 @@ public class StatementAnalyzer { insert, scope, Field.newUnqualified(Insert.ROWS, Insert.ROWS_TYPE, Insert.ROWS_CATEGORY)); } + @Override + public Scope visitInsertNumericRow(InsertNumericRow node, Optional<Scope> context) { + return visitInsert(node, context); + } + @Override public Scope visitInsertRow(InsertRow node, Optional<Scope> context) { return visitInsert(node, context); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java index 6f5f3c278ed..c047be7c2b4 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/RelationPlanner.java @@ -100,6 +100,7 @@ import org.apache.iotdb.db.queryengine.plan.planner.plan.node.pipe.PipeEnrichedW import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertNumericRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; @@ -130,11 +131,13 @@ import org.apache.iotdb.db.queryengine.plan.relational.planner.rowpattern.rowpat import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AsofJoinOn; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.AstVisitor; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.Delete; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertNumericRow; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertRow; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertRows; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertTablet; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.LoadTsFile; import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.PipeEnriched; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement; @@ -1370,6 +1373,26 @@ public class RelationPlanner implements AstVisitor<RelationPlan, Void> { insertNode, analysis.getRootScope(), Collections.emptyList(), outerContext); } + @Override + public RelationPlan visitInsertNumericRow(InsertNumericRow node, Void context) { + InsertNumericRowStatement statement = node.getInnerTreeStatement(); + String[] measurements = statement.getMeasurements(); + MeasurementSchema[] schemas = statement.getMeasurementSchemas(); + stayConsistent(measurements, schemas); + RelationalInsertNumericRowNode insert = + new RelationalInsertNumericRowNode( + idAllocator.genPlanNodeId(), + statement.getDevicePath(), + measurements, + statement.getDataTypes(), + schemas, + statement.getTime(), + statement.getNumericValues(), + statement.getColumnCategories()); + insert.setFailedMeasurementNumber(statement.getFailedMeasurementNumber()); + return new RelationPlan(insert, analysis.getRootScope(), Collections.emptyList(), outerContext); + } + @Override public RelationPlan visitInsertRow(InsertRow node, Void context) { InsertRowStatement insertRowStatement = node.getInnerTreeStatement(); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableModelPlanner.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableModelPlanner.java index cca616e0a3b..c91eb3e63fc 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableModelPlanner.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/planner/TableModelPlanner.java @@ -55,6 +55,7 @@ import org.apache.iotdb.db.queryengine.plan.scheduler.ClusterScheduler; import org.apache.iotdb.db.queryengine.plan.scheduler.IScheduler; import org.apache.iotdb.db.queryengine.plan.scheduler.load.LoadTsFileScheduler; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement; import org.apache.iotdb.rpc.RpcUtils; @@ -239,6 +240,19 @@ public class TableModelPlanner implements IPlanner { ((WrappedInsertStatement) statementToRedirect).getInnerTreeStatement(); if (!analysis.isFinishQueryAfterAnalyze()) { + if (insertStatement instanceof InsertNumericRowStatement) { + // A numeric row uses insertRecord, whose redirect response carries one endpoint. + if (tsstatus.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + List<TEndPoint> endpoints = analysis.getRedirectNodeList(); + if (endpoints != null + && !endpoints.isEmpty() + && !localEndPoint.equals(endpoints.get(0))) { + tsstatus.setCode(TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()); + tsstatus.setRedirectNode(endpoints.get(0)); + } + } + return; + } // Table Model Session supports insertTablet and pipe-generated insertRows statements. if (insertStatement instanceof InsertTabletStatement || insertStatement instanceof InsertRowsStatement) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertNumericRow.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertNumericRow.java new file mode 100644 index 00000000000..38f29f01d2c --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertNumericRow.java @@ -0,0 +1,71 @@ +/* + * 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.queryengine.plan.relational.sql.ast; + +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.IAstVisitor; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Relational analysis entry for a primitive numeric row. */ +public class InsertNumericRow extends WrappedInsertStatement { + public InsertNumericRow(InsertNumericRowStatement statement, MPPQueryContext context) { + super(statement, context); + } + + @Override + public <R, C> R accept(IAstVisitor<R, C> visitor, C context) { + return ((AstVisitor<R, C>) visitor).visitInsertNumericRow(this, context); + } + + @Override + public InsertNumericRowStatement getInnerTreeStatement() { + return (InsertNumericRowStatement) super.getInnerTreeStatement(); + } + + @Override + public void updateAfterSchemaValidation(MPPQueryContext context) { + // Numeric rows are already typed; schema validation casts values in their primitive storage. + } + + @Override + public String getTableName() { + return getInnerTreeStatement().getTableName(); + } + + @Override + public List<Object[]> getDeviceIdList() { + Object[] segments = getInnerTreeStatement().getTableDeviceID().getSegments(); + return Collections.singletonList(Arrays.copyOfRange(segments, 1, segments.length)); + } + + @Override + public List<String> getAttributeColumnNameList() { + return Collections.emptyList(); + } + + @Override + public List<Object[]> getAttributeValueList() { + return Collections.singletonList(new Object[0]); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java index 00d36a03e23..6488f76f56d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementType.java @@ -203,4 +203,5 @@ public enum StatementType { SHOW_EXTERNAL_SERVICE, SHOW_DISK_USAGE, + INSERT_NUMERIC_ROW, } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java index 5f2f72f4313..d107a5fcde8 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/StatementVisitor.java @@ -22,6 +22,7 @@ package org.apache.iotdb.db.queryengine.plan.statement; import org.apache.iotdb.db.queryengine.plan.statement.crud.DeleteDataStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertBaseStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertMultiTabletsStatement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertNumericRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsOfOneDeviceStatement; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowsStatement; @@ -405,6 +406,10 @@ public abstract class StatementVisitor<R, C> { return visitStatement(loadTsFileStatement, context); } + public R visitInsertNumericRow(InsertNumericRowStatement statement, C context) { + return visitInsertBase(statement, context); + } + public R visitInsertRow(InsertRowStatement insertRowStatement, C context) { return visitInsertBase(insertRowStatement, context); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertNumericRowStatement.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertNumericRowStatement.java new file mode 100644 index 00000000000..db90503d464 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertNumericRowStatement.java @@ -0,0 +1,292 @@ +/* + * 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.queryengine.plan.statement.crud; + +import org.apache.iotdb.calc.exception.QueryProcessException; +import org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot; +import org.apache.iotdb.commons.exception.SemanticException; +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchema; +import org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement; +import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory; +import org.apache.iotdb.commons.utils.TimePartitionUtils; +import org.apache.iotdb.db.i18n.DataNodeQueryMessages; +import org.apache.iotdb.db.pipe.resource.memory.InsertNodeMemoryEstimator; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaValidation; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.InsertNumericRow; +import org.apache.iotdb.db.queryengine.plan.statement.StatementType; +import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor; +import org.apache.iotdb.db.utils.NumericRowValues; + +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.IDeviceID; +import org.apache.tsfile.utils.RamUsageEstimator; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** A table-only numeric row. Schema edits and partial failures keep the payload in long[]. */ +public class InsertNumericRowStatement extends InsertBaseStatement { + private static final long INSTANCE_SIZE = + RamUsageEstimator.shallowSizeOfInstance(InsertNumericRowStatement.class); + private long time; + private NumericRowValues numericValues; + private IDeviceID deviceID; + + public InsertNumericRowStatement() { + statementType = StatementType.INSERT_NUMERIC_ROW; + writeToTable = true; + isAligned = true; + } + + public long getTime() { + return time; + } + + public void setTime(long time) { + this.time = time; + } + + public NumericRowValues getNumericValues() { + return numericValues; + } + + public long[] getValues() { + return numericValues.getValues(); + } + + public void setNumericValues(NumericRowValues values) { + numericValues = values; + deviceID = null; + } + + public int getValueCount() { + return numericValues == null ? 0 : numericValues.size(); + } + + /** RPC cells carry typed primitive FIELD bits or a length-prefixed STRING TAG. */ + public void fillValues(ByteBuffer buffer) throws QueryProcessException { + numericValues = new NumericRowValues(measurements.length); + dataTypes = new TSDataType[measurements.length]; + for (int i = 0; i < dataTypes.length; i++) { + byte type = buffer.get(); + if (type < 0 || type >= TSDataType.values().length) { + throw new QueryProcessException(DataNodeQueryMessages.UNSUPPORTED_DATA_TYPE + type); + } + dataTypes[i] = TSDataType.values()[type]; + numericValues.read(i, dataTypes[i], buffer); + } + } + + @Override + public <R, C> R accept(StatementVisitor<R, C> visitor, C context) { + return visitor.visitInsertNumericRow(this, context); + } + + @Override + public Statement toRelationalStatement(MPPQueryContext context) { + return new InsertNumericRow(this, context); + } + + @Override + public List<PartialPath> getPaths() { + return Collections.emptyList(); + } + + @Override + public long getMinTime() { + return time; + } + + public TTimePartitionSlot getTimePartitionSlot() { + return TimePartitionUtils.getTimePartitionSlot(time); + } + + @Override + public boolean isEmpty() { + return getValueCount() == 0; + } + + @Override + public Object getFirstValueOfIndex(int index) { + return index < 0 || index >= getValueCount() + ? null + : numericValues.getObject(index, getDataType(index)); + } + + @Override + public boolean isColumnPresent(int index) { + return super.isColumnPresent(index) && index < getValueCount(); + } + + @Override + protected boolean checkAndCastDataType(int index, TSDataType type) { + if (index < 0 + || index >= getValueCount() + || !numericValues.cast(index, getDataType(index), type)) { + return false; + } + dataTypes[index] = type; + return true; + } + + @Override + public void semanticCheck() { + super.semanticCheck(); + if (measurements.length != getValueCount()) { + throw new SemanticException( + String.format( + DataNodeQueryMessages + .THE_MEASUREMENTLIST_S_SIZE_D_IS_NOT_CONSISTENT_WITH_THE_VALUELIST_S_SIZE_D, + measurements.length, + getValueCount())); + } + } + + @Override + public void markFailedMeasurement(int index, Exception cause) { + if (!isColumnPresent(index)) { + return; + } + if (failedMeasurementIndex2Info == null) { + failedMeasurementIndex2Info = new HashMap<>(); + } + failedMeasurementIndex2Info.putIfAbsent( + index, + new FailedMeasurementInfo( + measurements[index], getDataType(index), getFirstValueOfIndex(index), cause)); + measurements[index] = null; + if (dataTypes != null && index < dataTypes.length) { + dataTypes[index] = null; + } + numericValues.setNull(index); + tagColumnIndices = null; + deviceID = null; + } + + @Override + public void removeAllFailedMeasurementMarks() { + if (failedMeasurementIndex2Info == null) { + return; + } + failedMeasurementIndex2Info.forEach( + (index, info) -> { + measurements[index] = info.getMeasurement(); + dataTypes[index] = info.getDataType(); + numericValues.setObject(index, info.getDataType(), info.getValue()); + }); + failedMeasurementIndex2Info.clear(); + tagColumnIndices = null; + deviceID = null; + } + + @Override + public void insertColumn(int pos, ColumnSchema columnSchema) { + super.insertColumn(pos, columnSchema); + int[] mapping = new int[measurements.length]; + for (int i = 0; i < mapping.length; i++) { + mapping[i] = i < pos ? i : i == pos ? -1 : i - 1; + } + numericValues = numericValues.remap(mapping); + deviceID = null; + } + + @Override + public void swapColumn(int src, int target) { + super.swapColumn(src, target); + numericValues.swap(src, target); + deviceID = null; + } + + @Override + public void rebuildArraysAfterExpansion(int[] mapping, String[] newMeasurements) { + super.rebuildArraysAfterExpansion(mapping, newMeasurements); + numericValues = numericValues.remap(mapping); + deviceID = null; + } + + @Override + protected void subRemoveAttributeColumns(List<Integer> columnsToKeep) { + numericValues = + numericValues.remap(columnsToKeep.stream().mapToInt(Integer::intValue).toArray()); + deviceID = null; + } + + @Override + public void setColumnCategories(TsTableColumnCategory[] categories) { + super.setColumnCategories(categories); + deviceID = null; + } + + public IDeviceID getTableDeviceID() { + if (deviceID == null) { + String[] segments = new String[getTagColumnIndices().size() + 1]; + segments[0] = getTableName(); + for (int i = 0; i < getTagColumnIndices().size(); i++) { + segments[i + 1] = numericValues.getTag(getTagColumnIndices().get(i)); + } + deviceID = IDeviceID.Factory.DEFAULT_FACTORY.create(segments); + } + return deviceID; + } + + @Override + protected long calculateBytesUsed() { + return INSTANCE_SIZE + + (numericValues == null ? 0 : numericValues.ramBytesUsed()) + + InsertNodeMemoryEstimator.sizeOfIDeviceID(deviceID); + } + + // Tree schema validation and logical view splitting do not apply to numeric table rows. + @Override + public ISchemaValidation getSchemaValidation() { + throw new UnsupportedOperationException(); + } + + @Override + public List<ISchemaValidation> getSchemaValidationList() { + throw new UnsupportedOperationException(); + } + + @Override + public InsertBaseStatement removeLogicalView() { + return this; + } + + @Override + public String getPipeLoggingString() { + return "InsertNumericRowStatement{table=" + + getTableName() + + ", time=" + + time + + ", valueLength=" + + getValueCount() + + '}'; + } + + @Override + public String toString() { + return getPipeLoggingString() + Arrays.toString(getValues()); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java index 1ac6c15488b..ba5349edc0e 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java @@ -1199,8 +1199,8 @@ public class StorageEngine implements IService { return () -> { if (!isReadyForNonReadWriteFunctions()) { return; - } - for (DataRegion dataRegion : dataRegionMap.values()) { + } + for (DataRegion dataRegion : dataRegionMap.values()) { if (dataRegion != null) { dataRegion.compactFileTimeIndexCache(); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java index de1a5a09b7d..df24cefdf30 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java @@ -42,6 +42,7 @@ import org.apache.iotdb.db.queryengine.common.DeviceContext; import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext; import org.apache.iotdb.db.queryengine.metric.QueryResourceMetricSet; import org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DataNodeTTLCache; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; @@ -80,6 +81,7 @@ import org.apache.iotdb.db.utils.CommonUtils; import org.apache.iotdb.db.utils.EncryptDBUtils; import org.apache.iotdb.db.utils.MemUtils; import org.apache.iotdb.db.utils.ModificationUtils; +import org.apache.iotdb.db.utils.NumericRowValues; import org.apache.iotdb.db.utils.datastructure.AlignedTVList; import org.apache.iotdb.db.utils.datastructure.TVList; import org.apache.iotdb.rpc.RpcUtils; @@ -290,7 +292,7 @@ public class TsFileProcessor { } } - private static void clearDataRegionReplicaSet(final InsertRowNode insertRowNode) { + private static void clearDataRegionReplicaSet(final AbstractInsertRowNode insertRowNode) { insertRowNode.setDataRegionReplicaSet(null); } @@ -310,7 +312,7 @@ public class TsFileProcessor { * * @param insertRowNode physical plan of insertion */ - public void insert(InsertRowNode insertRowNode, long[] infoForMetrics) + public void insert(AbstractInsertRowNode insertRowNode, long[] infoForMetrics) throws WriteProcessException { ensureMemTable(infoForMetrics); @@ -329,7 +331,7 @@ public class TsFileProcessor { insertRowNode.getDeviceID(), insertRowNode.getMeasurements(), insertRowNode.getDataTypes(), - insertRowNode.getValues(), + insertRowNode.getValuesForMemoryAccounting(), insertRowNode.getColumnCategories()); } else { memIncrements = @@ -337,7 +339,7 @@ public class TsFileProcessor { insertRowNode.getDeviceID(), insertRowNode.getMeasurements(), insertRowNode.getDataTypes(), - insertRowNode.getValues(), + insertRowNode.getValuesForMemoryAccounting(), insertRowNode.getColumnCategories()); } // recordScheduleMemoryBlockCost @@ -390,7 +392,7 @@ public class TsFileProcessor { if (insertRowNode.isAligned()) { pointInserted = workMemTable.insertAlignedRow(insertRowNode); } else { - pointInserted = workMemTable.insert(insertRowNode); + pointInserted = workMemTable.insert((InsertRowNode) insertRowNode); } } finally { reconcileAlignedTVListRamCost(alignedRamCostSnapshot, memIncrements[0]); @@ -514,7 +516,7 @@ public class TsFileProcessor { if (insertRowNode.isAligned()) { pointInserted += workMemTable.insertAlignedRow(insertRowNode); } else { - pointInserted += workMemTable.insert(insertRowNode); + pointInserted += workMemTable.insert((InsertRowNode) insertRowNode); } // update start time of this memtable tsFileResource.updateStartTime(insertRowNode.getDeviceID(), insertRowNode.getTime()); @@ -770,7 +772,7 @@ public class TsFileProcessor { IDeviceID deviceId, String[] measurements, TSDataType[] dataTypes, - Object[] values, + Object values, TsTableColumnCategory[] columnCategories) throws WriteProcessException { // Fixed-size TVList structures and materialized value primitive arrays. @@ -798,8 +800,8 @@ public class TsFileProcessor { } } // TEXT data mem size - if (dataTypes[i].isBinary() && values[i] != null) { - textDataIncrement += MemUtils.getBinarySize((Binary) values[i]); + if (dataTypes[i].isBinary() && values instanceof Object[] rowValues && rowValues[i] != null) { + textDataIncrement += MemUtils.getBinarySize((Binary) rowValues[i]); } } updateMemoryInfo(memTableIncrement, chunkMetadataIncrement, textDataIncrement); @@ -818,7 +820,7 @@ public class TsFileProcessor { for (InsertRowNode insertRowNode : insertRowNodeList) { IDeviceID deviceId = insertRowNode.getDeviceID(); TSDataType[] dataTypes = insertRowNode.getDataTypes(); - Object[] values = insertRowNode.getValues(); + Object values = insertRowNode.getValuesForMemoryAccounting(); String[] measurements = insertRowNode.getMeasurements(); for (int i = 0; dataTypes != null && i < dataTypes.length; i++) { // Skip failed Measurements @@ -852,8 +854,10 @@ public class TsFileProcessor { increasingMemTableInfo.get(deviceId).computeIfPresent(measurements[i], (k, v) -> v + 1); } // TEXT data mem size - if (dataTypes[i].isBinary() && values[i] != null) { - textDataIncrement += MemUtils.getBinarySize((Binary) values[i]); + if (dataTypes[i].isBinary() + && values instanceof Object[] rowValues + && rowValues[i] != null) { + textDataIncrement += MemUtils.getBinarySize((Binary) rowValues[i]); } } } @@ -866,7 +870,7 @@ public class TsFileProcessor { IDeviceID deviceId, String[] measurements, TSDataType[] dataTypes, - Object[] values, + Object values, TsTableColumnCategory[] columnCategories) throws WriteProcessException { // Fixed-size TVList structures and materialized value primitive arrays. @@ -915,9 +919,10 @@ public class TsFileProcessor { } for (int i = 0; dataTypes != null && i < dataTypes.length; i++) { if (isWritableFieldMeasurement(measurements, dataTypes, values, columnCategories, i) - && dataTypes[i].isBinary()) { + && dataTypes[i].isBinary() + && values instanceof Object[] rowValues) { // Binary values keep their variable-length content outside the fixed Binary[] array. - textDataIncrement += MemUtils.getBinarySize((Binary) values[i]); + textDataIncrement += MemUtils.getBinarySize((Binary) rowValues[i]); } } updateMemoryInfo(memTableIncrement, chunkMetadataIncrement, textDataIncrement); @@ -939,7 +944,7 @@ public class TsFileProcessor { for (InsertRowNode insertRowNode : insertRowNodeList) { IDeviceID deviceId = insertRowNode.getDeviceID(); TSDataType[] dataTypes = insertRowNode.getDataTypes(); - Object[] values = insertRowNode.getValues(); + Object values = insertRowNode.getValuesForMemoryAccounting(); String[] measurements = insertRowNode.getMeasurements(); IWritableMemChunk memChunk = @@ -1031,9 +1036,11 @@ public class TsFileProcessor { measurements, dataTypes, values, insertRowNode.getColumnCategories(), i)) { continue; } - if (dataTypes[i].isBinary() && values[i] != null) { + if (dataTypes[i].isBinary() + && values instanceof Object[] rowValues + && rowValues[i] != null) { // Binary values keep their variable-length content outside the fixed Binary[] array. - textDataIncrement += MemUtils.getBinarySize((Binary) values[i]); + textDataIncrement += MemUtils.getBinarySize((Binary) rowValues[i]); } } } @@ -1326,7 +1333,7 @@ public class TsFileProcessor { private static TSDataType[] getWritableFieldDataTypes( String[] measurementIds, TSDataType[] dataTypes, - Object[] valuesOrColumns, + Object valuesOrColumns, TsTableColumnCategory[] columnCategories) { List<TSDataType> writableFieldDataTypes = new ArrayList<>(); for (int i = 0; dataTypes != null && i < dataTypes.length; i++) { @@ -1341,13 +1348,15 @@ public class TsFileProcessor { private static boolean isWritableFieldMeasurement( String[] measurementIds, TSDataType[] dataTypes, - Object[] valuesOrColumns, + Object valuesOrColumns, TsTableColumnCategory[] columnCategories, int index) { return isFieldMeasurement(measurementIds, dataTypes, columnCategories, index) - && valuesOrColumns != null - && index < valuesOrColumns.length - && valuesOrColumns[index] != null; + && (valuesOrColumns instanceof NumericRowValues row + ? index < row.size() && !row.isNull(index) + : valuesOrColumns instanceof Object[] columns + && index < columns.length + && columns[index] != null); } private static boolean isFieldMeasurement( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALEntry.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALEntry.java index 8c0a5e35329..346e3007c7e 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALEntry.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALEntry.java @@ -22,9 +22,9 @@ package org.apache.iotdb.db.storageengine.dataregion.wal.buffer; import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode; import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType; import org.apache.iotdb.db.i18n.StorageEngineMessages; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ContinuousSameSearchIndexSeparatorNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; @@ -65,7 +65,7 @@ public abstract class WALEntry implements SerializedSize { this.value = value; if (value instanceof IMemTable) { this.type = WALEntryType.MEMORY_TABLE_SNAPSHOT; - } else if (value instanceof InsertRowNode) { + } else if (value instanceof AbstractInsertRowNode) { this.type = WALEntryType.INSERT_ROW_NODE; } else if (value instanceof InsertTabletNode) { this.type = WALEntryType.INSERT_TABLET_NODE; @@ -121,7 +121,7 @@ public abstract class WALEntry implements SerializedSize { value = AbstractMemTable.Factory.createFromOldMemTableSnapshot(stream); break; case INSERT_ROW_NODE: - value = (InsertRowNode) PlanNodeType.deserializeFromWAL(stream); + value = (AbstractInsertRowNode) PlanNodeType.deserializeFromWAL(stream); break; case INSERT_TABLET_NODE: value = (InsertTabletNode) PlanNodeType.deserializeFromWAL(stream); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALInfoEntry.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALInfoEntry.java index 957cf03d892..37cc8aff863 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALInfoEntry.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALInfoEntry.java @@ -22,9 +22,9 @@ package org.apache.iotdb.db.storageengine.dataregion.wal.buffer; import org.apache.iotdb.db.conf.IoTDBConfig; import org.apache.iotdb.db.conf.IoTDBDescriptor; import org.apache.iotdb.db.i18n.StorageEngineMessages; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; @@ -102,7 +102,7 @@ public class WALInfoEntry extends WALEntry { .serializeToWAL(buffer, tabletInfo.tabletRangeList, encodedSearchIndex); break; case INSERT_ROW_NODE: - ((InsertRowNode) value).serializeToWAL(buffer, encodedSearchIndex); + ((AbstractInsertRowNode) value).serializeToWAL(buffer, encodedSearchIndex); break; case INSERT_ROWS_NODE: ((InsertRowsNode) value).serializeToWAL(buffer, encodedSearchIndex); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/IWALNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/IWALNode.java index a8fbbee0dc4..b2689a5f99a 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/IWALNode.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/IWALNode.java @@ -21,9 +21,9 @@ package org.apache.iotdb.db.storageengine.dataregion.wal.node; import org.apache.iotdb.consensus.common.DataSet; import org.apache.iotdb.consensus.iot.log.ConsensusReqReader; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ContinuousSameSearchIndexSeparatorNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; @@ -38,7 +38,7 @@ import java.util.List; public interface IWALNode extends FlushListener, AutoCloseable, ConsensusReqReader, DataSet { /** Log InsertRowNode. */ - WALFlushListener log(long memTableId, InsertRowNode insertRowNode); + WALFlushListener log(long memTableId, AbstractInsertRowNode insertRowNode); /** Log InsertRowsNode. */ WALFlushListener log(long memTableId, InsertRowsNode insertRowsNode); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALFakeNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALFakeNode.java index d24d1a1d823..93ac79c05a9 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALFakeNode.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALFakeNode.java @@ -20,9 +20,9 @@ package org.apache.iotdb.db.storageengine.dataregion.wal.node; import org.apache.iotdb.db.i18n.StorageEngineMessages; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ContinuousSameSearchIndexSeparatorNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; @@ -53,7 +53,7 @@ public class WALFakeNode implements IWALNode { } @Override - public WALFlushListener log(long memTableId, InsertRowNode insertRowNode) { + public WALFlushListener log(long memTableId, AbstractInsertRowNode insertRowNode) { return getResult(); } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java index 0e66509d350..825a83ffe2c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java @@ -29,9 +29,9 @@ import org.apache.iotdb.consensus.common.request.IoTConsensusRequest; import org.apache.iotdb.db.conf.IoTDBConfig; import org.apache.iotdb.db.conf.IoTDBDescriptor; import org.apache.iotdb.db.i18n.StorageEngineMessages; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ContinuousSameSearchIndexSeparatorNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; -import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; @@ -222,7 +222,7 @@ public class WALNode implements IWALNode { } @Override - public WALFlushListener log(long memTableId, InsertRowNode insertRowNode) { + public WALFlushListener log(long memTableId, AbstractInsertRowNode insertRowNode) { logger.debug( StorageEngineMessages.STORAGE_LOG_WAL_NODE_LOGS_INSERTROWNODE_THE_SEARCH_INDEX_IS_027450AC, identifier, diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/file/TsFilePlanRedoer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/file/TsFilePlanRedoer.java index 221d39cdfcc..dbd26029418 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/file/TsFilePlanRedoer.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/file/TsFilePlanRedoer.java @@ -24,6 +24,7 @@ import org.apache.iotdb.commons.service.metric.MetricService; import org.apache.iotdb.commons.service.metric.enums.Metric; import org.apache.iotdb.commons.service.metric.enums.Tag; import org.apache.iotdb.db.exception.WriteProcessException; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.AbstractInsertRowNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; @@ -96,8 +97,8 @@ public class TsFilePlanRedoer { // the last chunk group may contain the same data with the logs, ignore such logs in seq file Optional<Long> lastEndTime = tsFileResource.getEndTime(node.getDeviceID()); long minTimeInNode; - if (node instanceof InsertRowNode) { - minTimeInNode = ((InsertRowNode) node).getTime(); + if (node instanceof AbstractInsertRowNode row) { + minTimeInNode = row.getTime(); } else { minTimeInNode = ((InsertTabletNode) node).getTimes()[0]; } @@ -107,9 +108,9 @@ public class TsFilePlanRedoer { } int pointsInserted; - if (node instanceof InsertRowNode) { + if (node instanceof AbstractInsertRowNode row) { if (node.isAligned()) { - pointsInserted = recoveryMemTable.insertAlignedRow((InsertRowNode) node); + pointsInserted = recoveryMemTable.insertAlignedRow(row); } else { pointsInserted = recoveryMemTable.insert((InsertRowNode) node); } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java index 5512e312cf0..7a56ac2ef03 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/parser/StatementGeneratorTest.java @@ -271,7 +271,7 @@ public class StatementGeneratorTest { Arrays.asList("s1", "s2"), ByteBuffer.wrap(new byte[] {0, 0, 0, 0}), 1000L); - InsertRowStatement statement = StatementGenerator.createStatement(req); + InsertRowStatement statement = (InsertRowStatement) StatementGenerator.createStatement(req); assertEquals(1000L, statement.getTime()); } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunkNumericTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunkNumericTest.java new file mode 100644 index 00000000000..8620f17f71b --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedWritableMemChunkNumericTest.java @@ -0,0 +1,134 @@ +/* + * 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.storageengine.dataregion.memtable; + +import org.apache.iotdb.db.utils.NumericRowValues; +import org.apache.iotdb.db.utils.datastructure.AlignedTVList; + +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class AlignedWritableMemChunkNumericTest { + + @Test + public void testMappingAllocatedOnlyForNumericRowsAndReused() throws Exception { + // Ordinary row/Tablet writes must not allocate scratch space; numeric rows share it. + List<IMeasurementSchema> schemas = + Collections.singletonList(new MeasurementSchema("a", TSDataType.INT64)); + AlignedWritableMemChunk chunk = new AlignedWritableMemChunk(new ArrayList<>(schemas), true); + try { + assertNull(mapping(chunk)); + chunk.writeAlignedPoints(1, new Object[] {10L}, schemas); + chunk.writeAlignedTablet( + new long[] {2}, new Object[] {new long[] {20}}, null, schemas, 0, 1, null); + assertNull(mapping(chunk)); + + chunk.writeAlignedNumericPoints(3, row(30), schemas); + int[] mapping = mapping(chunk); + assertNotNull(mapping); + chunk.writeAlignedNumericPoints(4, row(40), schemas); + assertSame(mapping, mapping(chunk)); + assertEquals(30, chunk.getWorkingTVList().getLongByValueIndex(2, 0)); + assertEquals(40, chunk.getWorkingTVList().getLongByValueIndex(3, 0)); + } finally { + chunk.release(); + } + } + + @Test + public void testMappingResetAndGrowthWithMixedWrites() throws Exception { + // Reordered/absent columns must stay correct after reuse and schema growth by either API. + IMeasurementSchema a = new MeasurementSchema("a", TSDataType.INT64); + IMeasurementSchema b = new MeasurementSchema("b", TSDataType.INT64); + IMeasurementSchema c = new MeasurementSchema("c", TSDataType.INT64); + IMeasurementSchema d = new MeasurementSchema("d", TSDataType.INT64); + AlignedWritableMemChunk chunk = + new AlignedWritableMemChunk(new ArrayList<>(Arrays.asList(a, b)), true); + try { + chunk.writeAlignedNumericPoints(1, row(20, 10), Arrays.asList(b, a)); + int[] mapping = mapping(chunk); + chunk.writeAlignedNumericPoints(2, row(30, 999), Arrays.asList(a, null)); + assertSame(mapping, mapping(chunk)); + AlignedTVList list = chunk.getWorkingTVList(); + assertEquals(10, list.getLongByValueIndex(0, 0)); + assertEquals(20, list.getLongByValueIndex(0, 1)); + assertEquals(30, list.getLongByValueIndex(1, 0)); + assertTrue(list.isNullValue(1, 1)); + + chunk.writeAlignedNumericPoints(3, row(40, 50), Arrays.asList(c, b)); + int[] extendedMapping = mapping(chunk); + assertEquals(3, extendedMapping.length); + assertTrue(list.isNullValue(2, 0)); + assertEquals(50, list.getLongByValueIndex(2, 1)); + assertEquals(40, list.getLongByValueIndex(2, 2)); + + chunk.writeAlignedTablet( + new long[] {4}, + new Object[] {new long[] {60}}, + null, + Collections.singletonList(d), + 0, + 1, + null); + assertSame(extendedMapping, mapping(chunk)); + chunk.writeAlignedNumericPoints(5, row(70), Collections.singletonList(a)); + assertEquals(4, mapping(chunk).length); + assertEquals(60, list.getLongByValueIndex(3, 3)); + assertEquals(70, list.getLongByValueIndex(4, 0)); + assertTrue(list.isNullValue(4, 1)); + assertTrue(list.isNullValue(4, 2)); + assertTrue(list.isNullValue(4, 3)); + + mapping = mapping(chunk); + chunk.writeAlignedNumericPoints(6, row(80), Collections.singletonList(d)); + assertSame(mapping, mapping(chunk)); + assertTrue(list.isNullValue(5, 0)); + assertEquals(80, list.getLongByValueIndex(5, 3)); + } finally { + chunk.release(); + } + } + + private static NumericRowValues row(long... values) { + return new NumericRowValues(values, new BitSet()); + } + + private static int[] mapping(AlignedWritableMemChunk chunk) throws Exception { + // Array identity verifies the allocation contract without exposing scratch state as public API. + Field field = AlignedWritableMemChunk.class.getDeclaredField("numericColumnMapping"); + field.setAccessible(true); + return (int[]) field.get(chunk); + } +} diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java index e36796fca43..64da340870e 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/queryengine/plan/planner/plan/node/PlanNodeType.java @@ -209,6 +209,7 @@ public enum PlanNodeType { RELATIONAL_INSERT_ROWS((short) 2002), RELATIONAL_DELETE_DATA((short) 2003), OBJECT_FILE_NODE((short) 2004), + RELATIONAL_INSERT_NUMERIC_ROW((short) 2005), ; private static final IPlanNodeDeserializer DESERIALIZER;
