diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java index 3fb60b8760da8..5805fb103309e 100644 --- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java +++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java @@ -43,6 +43,7 @@ import java.nio.ByteBuffer; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; @@ -260,6 +261,95 @@ public void testQuery() throws Exception { verify(fetchResultsResp, times(0)).getStatus(); } + /** + * The tree-model time pseudo-column is addressed by a negative TsBlock column index. getLong, + * getString and getObject handle it; the strictly-typed getters used to fall through to + * TsBlock.getColumn(-1) and escape as an unchecked ArrayIndexOutOfBoundsException instead of the + * SQLException a JDBC caller can handle. getDate delegates to getInt, so it is affected too. + */ + @SuppressWarnings("resource") + @Test + public void testTimeColumnRejectedByStrictlyTypedGetters() throws Exception { + try (ResultSet resultSet = executeAndFetchTreeModelResultSet()) { + Assert.assertEquals(1, resultSet.findColumn("Time")); + Assert.assertTrue(resultSet.next()); + + // The getters that support the time column keep working. + Assert.assertEquals(2L, resultSet.getLong(1)); + Assert.assertEquals("2", resultSet.getString(1)); + Assert.assertEquals(new Timestamp(2), resultSet.getObject(1)); + Assert.assertEquals(new Timestamp(2), resultSet.getTimestamp(1)); + + // The strictly-typed ones must report a SQLException, not an unchecked exception. + assertTimeColumnRejected(() -> resultSet.getBoolean(1)); + assertTimeColumnRejected(() -> resultSet.getInt(1)); + assertTimeColumnRejected(() -> resultSet.getFloat(1)); + assertTimeColumnRejected(() -> resultSet.getDouble(1)); + assertTimeColumnRejected(() -> resultSet.getDate(1)); + + // Reading by name goes through the same path. + assertTimeColumnRejected(() -> resultSet.getInt("Time")); + } + } + + @FunctionalInterface + private interface ResultSetRead { + void run() throws SQLException; + } + + private void assertTimeColumnRejected(ResultSetRead read) { + try { + read.run(); + Assert.fail("reading the time column with a strictly-typed getter should have thrown"); + } catch (SQLException e) { + // expected + } catch (RuntimeException e) { + Assert.fail( + "expected a SQLException but the time column leaked an unchecked " + + e.getClass().getName() + + ": " + + e.getMessage()); + } + } + + /** Same tree-model fixture as testQuery: column 1 is Time, columns 2..5 are measurements. */ + private ResultSet executeAndFetchTreeModelResultSet() throws Exception { + List columns = new ArrayList<>(); + columns.add("root.vehicle.d0.s2"); + columns.add("root.vehicle.d0.s1"); + columns.add("root.vehicle.d0.s0"); + columns.add("root.vehicle.d0.s2"); + + List dataTypeList = new ArrayList<>(); + dataTypeList.add("FLOAT"); + dataTypeList.add("INT64"); + dataTypeList.add("INT32"); + dataTypeList.add("FLOAT"); + + when(execResp.isSetColumns()).thenReturn(true); + when(execResp.getColumns()).thenReturn(columns); + when(execResp.isSetDataTypeList()).thenReturn(true); + when(execResp.getDataTypeList()).thenReturn(dataTypeList); + when(execResp.isSetOperationType()).thenReturn(true); + when(execResp.getOperationType()).thenReturn("QUERY"); + when(execResp.isSetQueryId()).thenReturn(true); + when(execResp.getQueryId()).thenReturn(queryId); + when(execResp.isSetTableModel()).thenReturn(false); + when(execResp.isIgnoreTimeStamp()).thenReturn(false); + + List columnIndex2TsBlockColumnIndexList = new ArrayList<>(columns.size()); + columnIndex2TsBlockColumnIndexList.add(0); + columnIndex2TsBlockColumnIndexList.add(1); + columnIndex2TsBlockColumnIndexList.add(2); + columnIndex2TsBlockColumnIndexList.add(0); + when(execResp.getColumnIndex2TsBlockColumnIndexList()) + .thenReturn(columnIndex2TsBlockColumnIndexList); + + Assert.assertTrue(statement.execute("select * from root.vehicle.d0")); + fetchResultsResp.hasResultSet = true; + return statement.getResultSet(); + } + private void constructObjectList(List standardObject) { Object[][] input = { { diff --git a/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java b/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java index d8612aa66a75e..b9d76aa4fd957 100644 --- a/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java +++ b/iotdb-client/service-rpc/src/main/i18n/en/org/apache/iotdb/rpc/i18n/RpcMessages.java @@ -68,6 +68,16 @@ public final class RpcMessages { "column index %d out of range %d"; public static final String UNKNOWN_COLUMN_NAME = "Unknown column name: "; public static final String NO_RECORD_REMAINS = "No record remains"; + public static final String CANNOT_READ_BOOLEAN_FROM_TIME_COLUMN = + "Cannot read boolean from time column"; + public static final String CANNOT_READ_DOUBLE_FROM_TIME_COLUMN = + "Cannot read double from time column"; + public static final String CANNOT_READ_FLOAT_FROM_TIME_COLUMN = + "Cannot read float from time column"; + public static final String CANNOT_READ_INT32_FROM_TIME_COLUMN = + "Cannot read int32 from time column"; + public static final String CANNOT_READ_BINARY_FROM_TIME_COLUMN = + "Cannot read binary from time column"; public static final String CANNOT_CLOSE_DATASET = "Cannot close dataset, because of network connection: {} "; diff --git a/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java b/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java index c340b54c76409..5ddfe9fc0b27e 100644 --- a/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java +++ b/iotdb-client/service-rpc/src/main/i18n/zh/org/apache/iotdb/rpc/i18n/RpcMessages.java @@ -58,6 +58,11 @@ public final class RpcMessages { public static final String COLUMN_INDEX_OUT_OF_RANGE = "列索引 %d 超出范围 %d"; public static final String UNKNOWN_COLUMN_NAME = "未知列名:"; public static final String NO_RECORD_REMAINS = "没有剩余记录"; + public static final String CANNOT_READ_BOOLEAN_FROM_TIME_COLUMN = "无法从时间列读取 boolean 值"; + public static final String CANNOT_READ_DOUBLE_FROM_TIME_COLUMN = "无法从时间列读取 double 值"; + public static final String CANNOT_READ_FLOAT_FROM_TIME_COLUMN = "无法从时间列读取 float 值"; + public static final String CANNOT_READ_INT32_FROM_TIME_COLUMN = "无法从时间列读取 int32 值"; + public static final String CANNOT_READ_BINARY_FROM_TIME_COLUMN = "无法从时间列读取 binary 值"; public static final String CANNOT_CLOSE_DATASET = "无法关闭数据集,网络连接异常:{} "; // RpcUtils diff --git a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/IoTDBRpcDataSet.java b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/IoTDBRpcDataSet.java index 7619be16734d6..56200399b4e64 100644 --- a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/IoTDBRpcDataSet.java +++ b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/IoTDBRpcDataSet.java @@ -330,9 +330,16 @@ public boolean getBoolean(String columnName) throws StatementExecutionException return getBooleanByTsBlockColumnIndex(getTsBlockColumnIndexForColumnName(columnName)); } + // A negative tsBlockColumnIndex denotes the tree-model time pseudo-column. Only getLong, + // getString and getObject can serve it; the strictly-typed getters reject it here so that + // callers see a StatementExecutionException instead of an ArrayIndexOutOfBoundsException + // escaping from TsBlock.getColumn(-1). private boolean getBooleanByTsBlockColumnIndex(int tsBlockColumnIndex) throws StatementExecutionException { checkRecord(); + if (tsBlockColumnIndex < 0) { + throw new StatementExecutionException(RpcMessages.CANNOT_READ_BOOLEAN_FROM_TIME_COLUMN); + } if (!isNull(tsBlockColumnIndex, tsBlockIndex)) { lastReadWasNull = false; return curTsBlock.getColumn(tsBlockColumnIndex).getBoolean(tsBlockIndex); @@ -353,6 +360,9 @@ public double getDouble(String columnName) throws StatementExecutionException { private double getDoubleByTsBlockColumnIndex(int tsBlockColumnIndex) throws StatementExecutionException { checkRecord(); + if (tsBlockColumnIndex < 0) { + throw new StatementExecutionException(RpcMessages.CANNOT_READ_DOUBLE_FROM_TIME_COLUMN); + } if (!isNull(tsBlockColumnIndex, tsBlockIndex)) { lastReadWasNull = false; return curTsBlock.getColumn(tsBlockColumnIndex).getDouble(tsBlockIndex); @@ -373,6 +383,9 @@ public float getFloat(String columnName) throws StatementExecutionException { private float getFloatByTsBlockColumnIndex(int tsBlockColumnIndex) throws StatementExecutionException { checkRecord(); + if (tsBlockColumnIndex < 0) { + throw new StatementExecutionException(RpcMessages.CANNOT_READ_FLOAT_FROM_TIME_COLUMN); + } if (!isNull(tsBlockColumnIndex, tsBlockIndex)) { lastReadWasNull = false; return curTsBlock.getColumn(tsBlockColumnIndex).getFloat(tsBlockIndex); @@ -393,6 +406,9 @@ public int getInt(String columnName) throws StatementExecutionException { private int getIntByTsBlockColumnIndex(int tsBlockColumnIndex) throws StatementExecutionException { checkRecord(); + if (tsBlockColumnIndex < 0) { + throw new StatementExecutionException(RpcMessages.CANNOT_READ_INT32_FROM_TIME_COLUMN); + } if (!isNull(tsBlockColumnIndex, tsBlockIndex)) { lastReadWasNull = false; TSDataType type = curTsBlock.getColumn(tsBlockColumnIndex).getDataType(); @@ -451,6 +467,9 @@ public Binary getBinary(String columnName) throws StatementExecutionException { private Binary getBinaryTsBlockColumnIndex(int tsBlockColumnIndex) throws StatementExecutionException { checkRecord(); + if (tsBlockColumnIndex < 0) { + throw new StatementExecutionException(RpcMessages.CANNOT_READ_BINARY_FROM_TIME_COLUMN); + } if (!isNull(tsBlockColumnIndex, tsBlockIndex)) { lastReadWasNull = false; return curTsBlock.getColumn(tsBlockColumnIndex).getBinary(tsBlockIndex);