This is an automated email from the ASF dual-hosted git repository. JackieTien97 pushed a commit to branch fix/jdbc-table-model-metadata-cancel in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 4099bc4b3e828443264494abcec5b7eea74885f3 Author: JackieTien97 <[email protected]> AuthorDate: Wed Oct 21 11:59:09 2026 +0800 [JDBC] Fix table-model metadata, prepared cancel/fetch size and getObject(Class) IoTDBRelationalDatabaseMetadata.getTables / getColumns: - Honor tableNamePattern, columnNamePattern and types. A null pattern no longer turns into LIKE 'null', and quotes in patterns are escaped. - Report the real database and table names in TABLE_SCHEM / TABLE_NAME instead of the (escaped) search patterns; add TABLE_CAT to getTables and report it as NULL in both, since the table model has no catalogs. - Return an empty result set instead of null, and never send an empty TsBlock, which the result set read as one bogus row. - Derive NULLABLE / IS_NULLABLE from the TIME category instead of the column name, and number ORDINAL_POSITION within each table. - Apply the same patterns in the SHOW / DESC fallback used when information_schema cannot be queried, and fix removeEscape, which never removed an escape. IoTDBTablePreparedStatement: - Record the query id of prepared executions so that cancel() reaches the server, and reset the cancelled state for every execution. - Send the statement fetch size (capped by maxRows) with TSExecutePreparedReq. - Convert setTimestamp values to the connection's time precision (ms / us / ns), rejecting values the precision cannot represent. IoTDBJDBCResultSet: - Implement getObject(int, Class) for String, Boolean, Integer, Long, Float, Double, byte[], Timestamp and LocalDate. --- .../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 4 + .../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 4 + .../iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java | 7 +- .../org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java | 51 +- .../java/org/apache/iotdb/jdbc/IoTDBStatement.java | 27 +- .../iotdb/jdbc/IoTDBTablePreparedStatement.java | 28 +- .../IoTDBRelationalDatabaseMetadata.java | 453 +++++++++++------ .../apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java | 158 ++++++ .../jdbc/IoTDBRelationalDatabaseMetadataTest.java | 539 +++++++++++++++++++++ .../jdbc/IoTDBTablePreparedStatementTest.java | 155 ++++++ 10 files changed, 1263 insertions(+), 163 deletions(-) diff --git a/iotdb-client/jdbc/src/main/i18n/en/org/apache/iotdb/jdbc/i18n/JdbcMessages.java b/iotdb-client/jdbc/src/main/i18n/en/org/apache/iotdb/jdbc/i18n/JdbcMessages.java index 17f33cdf922..ab0206f33ed 100644 --- a/iotdb-client/jdbc/src/main/i18n/en/org/apache/iotdb/jdbc/i18n/JdbcMessages.java +++ b/iotdb-client/jdbc/src/main/i18n/en/org/apache/iotdb/jdbc/i18n/JdbcMessages.java @@ -225,5 +225,9 @@ public final class JdbcMessages { "Can not establish connection with %s : You may try to connect an old version IoTDB instance" + " using a client with new version: %s. "; public static final String EXCEPTION_CAN_NOT_ESTABLISH_CONNECTION_ARG_ARG_D7246055 = "Can not establish connection with %s : %s. "; + public static final String EXCEPTION_TIMESTAMP_ARG_OUT_RANGE_TIME_PRECISION_ARG_E25E3001 = "Timestamp %s is out of range for the time precision %s"; + public static final String EXCEPTION_CANNOT_CONVERT_ARG_COLUMN_ARG_FBFBC158 = "Cannot convert a %s column to %s"; + public static final String EXCEPTION_TARGET_TYPE_GETOBJECT_MUST_NOT_NULL_2E310BEC = "The target type of getObject must not be null"; + public static final String LOG_SHOW_TABLES_ERROR_ARG_068CF1BA = "Show tables error: {}"; } diff --git a/iotdb-client/jdbc/src/main/i18n/zh/org/apache/iotdb/jdbc/i18n/JdbcMessages.java b/iotdb-client/jdbc/src/main/i18n/zh/org/apache/iotdb/jdbc/i18n/JdbcMessages.java index a1cc83ea5e6..35efaac0944 100644 --- a/iotdb-client/jdbc/src/main/i18n/zh/org/apache/iotdb/jdbc/i18n/JdbcMessages.java +++ b/iotdb-client/jdbc/src/main/i18n/zh/org/apache/iotdb/jdbc/i18n/JdbcMessages.java @@ -224,5 +224,9 @@ public final class JdbcMessages { "无法与 %s 建立连接:可能正在使用新版本客户端连接旧版本 IoTDB 实例:" + "%s。"; public static final String EXCEPTION_CAN_NOT_ESTABLISH_CONNECTION_ARG_ARG_D7246055 = "无法与 %s 建立连接:%s。"; + public static final String EXCEPTION_TIMESTAMP_ARG_OUT_RANGE_TIME_PRECISION_ARG_E25E3001 = "时间戳 %s 超出了时间精度 %s 可表示的范围"; + public static final String EXCEPTION_CANNOT_CONVERT_ARG_COLUMN_ARG_FBFBC158 = "无法将 %s 类型的列转换为 %s"; + public static final String EXCEPTION_TARGET_TYPE_GETOBJECT_MUST_NOT_NULL_2E310BEC = "getObject 的目标类型不能为 null"; + public static final String LOG_SHOW_TABLES_ERROR_ARG_068CF1BA = "查询表信息出错:{}"; } diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java index 0c50512e80b..1b0cc9166b4 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java @@ -415,10 +415,15 @@ public abstract class IoTDBAbstractDatabaseMetadata implements DatabaseMetaData for (List<Object> valuesInRow : valuesList) { tsBlockBuilder.getTimeColumnBuilder().writeLong(0); for (int j = 0; j < tsDataTypeList.size(); j++) { + Object value = valuesInRow.get(j); + if (value == null) { + tsBlockBuilder.getColumnBuilder(j).appendNull(); + continue; + } TSDataType columnType = tsDataTypeList.get(j); COLUMN_VALUE_WRITER_SERVICE .call(Type.fromTsDataType(columnType)) - .write(tsBlockBuilder.getColumnBuilder(j), valuesInRow.get(j)); + .write(tsBlockBuilder.getColumnBuilder(j), value); } tsBlockBuilder.declarePosition(); } diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java index 3ea3c52320d..138d22d56bd 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java @@ -60,6 +60,7 @@ import java.sql.SQLXML; import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; import java.time.ZoneId; import java.util.Calendar; import java.util.List; @@ -645,8 +646,54 @@ public class IoTDBJDBCResultSet implements ResultSet { } @Override - public <T> T getObject(int arg0, Class<T> arg1) throws SQLException { - throw new SQLException(Constant.METHOD_NOT_SUPPORTED); + public <T> T getObject(int columnIndex, Class<T> type) throws SQLException { + if (type == null) { + throw new SQLException(JdbcMessages.EXCEPTION_TARGET_TYPE_GETOBJECT_MUST_NOT_NULL_2E310BEC); + } + try { + // Rejects a bad index before getDataType fails on it with an unchecked exception + ioTDBRpcDataSet.findColumnNameByIndex(columnIndex); + final TSDataType dataType = ioTDBRpcDataSet.getDataType(columnIndex); + final Object value; + if (type == String.class) { + value = ioTDBRpcDataSet.getString(columnIndex); + } else if (type == byte[].class) { + value = getBytes(columnIndex); + } else if (type == Timestamp.class && dataType == TSDataType.TIMESTAMP) { + value = ioTDBRpcDataSet.getTimestamp(columnIndex); + } else if (type == LocalDate.class && dataType == TSDataType.DATE) { + value = ioTDBRpcDataSet.getDate(columnIndex); + } else if (type == Boolean.class && dataType == TSDataType.BOOLEAN) { + value = nullIfLastReadWasNull(ioTDBRpcDataSet.getBoolean(columnIndex)); + } else if (type == Integer.class && dataType == TSDataType.INT32) { + value = nullIfLastReadWasNull(ioTDBRpcDataSet.getInt(columnIndex)); + } else if (type == Long.class + && (dataType == TSDataType.INT32 + || dataType == TSDataType.INT64 + || dataType == TSDataType.TIMESTAMP)) { + value = nullIfLastReadWasNull(ioTDBRpcDataSet.getLong(columnIndex)); + } else if (type == Float.class && dataType == TSDataType.FLOAT) { + value = nullIfLastReadWasNull(ioTDBRpcDataSet.getFloat(columnIndex)); + } else if (type == Double.class && dataType == TSDataType.FLOAT) { + value = nullIfLastReadWasNull((double) ioTDBRpcDataSet.getFloat(columnIndex)); + } else if (type == Double.class && dataType == TSDataType.DOUBLE) { + value = nullIfLastReadWasNull(ioTDBRpcDataSet.getDouble(columnIndex)); + } else { + throw new SQLException( + String.format( + JdbcMessages.EXCEPTION_CANNOT_CONVERT_ARG_COLUMN_ARG_FBFBC158, + dataType, + type.getName())); + } + return type.cast(value); + } catch (StatementExecutionException e) { + throw new SQLException(e.getMessage()); + } + } + + // The primitive getters return a default value such as 0 for SQL NULL + private Object nullIfLastReadWasNull(Object value) { + return ioTDBRpcDataSet.isLastReadWasNull() ? null : value; } @Override diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBStatement.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBStatement.java index 2af6e970a92..260ef79724c 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBStatement.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBStatement.java @@ -357,11 +357,7 @@ public class IoTDBStatement implements Statement { private boolean executeSQL(String sql) throws TException, SQLException { isCancelled = false; TSExecuteStatementReq execReq = new TSExecuteStatementReq(sessionId, sql, stmtId); - int rows = fetchSize; - if (maxRows != 0 && fetchSize > maxRows) { - rows = maxRows; - } - execReq.setFetchSize(rows); + execReq.setFetchSize(getRequestFetchSize()); execReq.setTimeout((long) queryTimeout * 1000); TSExecuteStatementResp execResp = callWithRetryAndReconnect( @@ -510,11 +506,7 @@ public class IoTDBStatement implements Statement { private ResultSet executeQuerySQL(String sql, long timeoutInMS) throws TException, SQLException { isCancelled = false; TSExecuteStatementReq execReq = new TSExecuteStatementReq(sessionId, sql, stmtId); - int rows = fetchSize; - if (maxRows != 0 && fetchSize > maxRows) { - rows = maxRows; - } - execReq.setFetchSize(rows); + execReq.setFetchSize(getRequestFetchSize()); execReq.setTimeout(timeoutInMS); execReq.setJdbcQuery(true); TSExecuteStatementResp execResp = @@ -825,6 +817,21 @@ public class IoTDBStatement implements Statement { return stmtId; } + /** The fetch size to send with an execution request, capped by maxRows when that is set. */ + int getRequestFetchSize() { + return maxRows != 0 && fetchSize > maxRows ? maxRows : fetchSize; + } + + /** Lets {@link #cancel()} reach the server again; called when a new execution starts. */ + void clearCancelled() { + isCancelled = false; + } + + /** Records the server-side query of the latest execution, which {@link #cancel()} cancels. */ + void setQueryId(long queryId) { + this.queryId = queryId; + } + public long getMilliSecond(long time) { return RpcUtils.getMilliSecond(time, connection.getTimeFactor()); } diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatement.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatement.java index 2fd0f382435..e0822a32ea9 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatement.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatement.java @@ -199,12 +199,15 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa req.setParameters( PreparedParameterSerde.serialize(parameterValues, parameterTypes, parameterCount)); req.setStatementId(getStmtId()); + req.setFetchSize(getRequestFetchSize()); if (queryTimeout > 0) { req.setTimeout(queryTimeout * 1000L); } + clearCancelled(); try { TSExecuteStatementResp resp = client.executePreparedStatement(req); + setQueryId(resp.isSetQueryId() ? resp.getQueryId() : -1); RpcUtils.verifySuccess(resp.getStatus()); return resp; } catch (TException | StatementExecutionException e) { @@ -446,10 +449,27 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa @Override public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { checkParameterIndex(parameterIndex); - // Use millisecond value for SQL compatibility with TIMESTAMP columns - long timestampMs = x.getTime(); - setPreparedParameterValue(parameterIndex, timestampMs, Types.BIGINT); - this.parameters.put(parameterIndex, Long.toString(timestampMs)); + // The server reads time values in its own precision (ms, us or ns) + long time = toServerTime(x); + setPreparedParameterValue(parameterIndex, time, Types.BIGINT); + this.parameters.put(parameterIndex, Long.toString(time)); + } + + private long toServerTime(Timestamp x) throws SQLException { + int timeFactor = getTimeFactor(); + // getTime() carries the milliseconds of getNanos(), so only its whole seconds are used + long seconds = Math.floorDiv(x.getTime(), 1_000L); + try { + return Math.addExact( + Math.multiplyExact(seconds, timeFactor), x.getNanos() / (1_000_000_000 / timeFactor)); + } catch (ArithmeticException e) { + throw new SQLException( + String.format( + JdbcMessages.EXCEPTION_TIMESTAMP_ARG_OUT_RANGE_TIME_PRECISION_ARG_E25E3001, + x, + RpcUtils.getTimePrecision(timeFactor)), + e); + } } @Override diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/relational/IoTDBRelationalDatabaseMetadata.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/relational/IoTDBRelationalDatabaseMetadata.java index 7ddfca1c01a..109b20b7ec8 100644 --- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/relational/IoTDBRelationalDatabaseMetadata.java +++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/relational/IoTDBRelationalDatabaseMetadata.java @@ -45,6 +45,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.regex.Pattern; public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetadata { @@ -56,7 +57,13 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada ? IoTDBRelationalDatabaseMetadata.class.getPackage().getImplementationVersion() : "UNKNOWN"; - public static final String SHOW_TABLES_ERROR_MSG = "Show tables error: {}"; + public static final String SHOW_TABLES_ERROR_MSG = + JdbcMessages.LOG_SHOW_TABLES_ERROR_ARG_068CF1BA; + + private static final String TABLE_TYPE_TABLE = "TABLE"; + private static final String TIME_CATEGORY = "TIME"; + // Same as getSearchStringEscape() + private static final char SEARCH_STRING_ESCAPE = '\\'; public static final String[] allIotdbTableSQLKeywords = { "ALTER", @@ -207,117 +214,52 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada throws SQLException { Statement stmt = this.connection.createStatement(); - boolean legacyMode = true; - - ResultSet rs; + List<TableInfo> tables; try { - String sql = - String.format( - "select * from information_schema.tables where database like '%s' escape '\\'", - schemaPattern); - rs = stmt.executeQuery(sql); - } catch (SQLException e) { - LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); - - try { - String sql = String.format("show tables details from %s", removeEscape(schemaPattern)); - rs = stmt.executeQuery(sql); - legacyMode = false; - } catch (SQLException e1) { - LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); - throw e; - } + // Every table is reported as TABLE, so a type filter without TABLE matches nothing + tables = + isTableTypeRequested(types) + ? listTables(stmt, schemaPattern, tableNamePattern) + : Collections.emptyList(); } finally { stmt.close(); } // Setup Fields - Field[] fields = new Field[6]; - fields[0] = new Field("", TABLE_SCHEM, "TEXT"); - fields[1] = new Field("", TABLE_NAME, "TEXT"); - fields[2] = new Field("", TABLE_TYPE, "TEXT"); - fields[3] = new Field("", REMARKS, "TEXT"); - fields[4] = new Field("", COLUMN_SIZE, INT32); - fields[5] = new Field("", DECIMAL_DIGITS, INT32); + Field[] fields = new Field[7]; + fields[0] = new Field("", TABLE_CAT, "TEXT"); + fields[1] = new Field("", TABLE_SCHEM, "TEXT"); + fields[2] = new Field("", TABLE_NAME, "TEXT"); + fields[3] = new Field("", TABLE_TYPE, "TEXT"); + fields[4] = new Field("", REMARKS, "TEXT"); + fields[5] = new Field("", COLUMN_SIZE, INT32); + fields[6] = new Field("", DECIMAL_DIGITS, INT32); List<TSDataType> tsDataTypeList = Arrays.asList( TSDataType.TEXT, TSDataType.TEXT, TSDataType.TEXT, TSDataType.TEXT, + TSDataType.TEXT, TSDataType.INT32, TSDataType.INT32); - List<String> columnNameList = new ArrayList<>(); - List<String> columnTypeList = new ArrayList<>(); - Map<String, Integer> columnNameIndex = new HashMap<>(); - List<List<Object>> valuesList = new ArrayList<>(); - for (int i = 0; i < fields.length; i++) { - columnNameList.add(fields[i].getName()); - columnTypeList.add(fields[i].getSqlType()); - columnNameIndex.put(fields[i].getName(), i); - } // Extract Values - boolean hasResultSet = false; - while (rs.next()) { - hasResultSet = true; + List<List<Object>> valuesList = new ArrayList<>(); + for (TableInfo table : tables) { List<Object> valueInRow = new ArrayList<>(); - for (int i = 0; i < fields.length; i++) { - if (i == 0) { - valueInRow.add(schemaPattern); - } else if (i == 1) { - // valueInRow.add(rs.getString(2)); - valueInRow.add(legacyMode ? rs.getString("table_name") : rs.getString("TableName")); - } else if (i == 2) { - valueInRow.add("TABLE"); - } else if (i == 3) { - // String tgtString = ""; - // String ttl = rs.getString("ttl(ms)"); - // tgtString += "TTL(ms): " + ttl; - String comment = legacyMode ? rs.getString("comment") : rs.getString("Comment"); - if (comment != null && !comment.isEmpty()) { - valueInRow.add(comment); - } else { - valueInRow.add(""); - } - } else if (i == 4) { - valueInRow.add(getTypePrecision(fields[i].getSqlType())); - } else if (i == 5) { - valueInRow.add(getTypeScale(fields[i].getSqlType())); - } else { - valueInRow.add("TABLE"); - } - } + // TABLE_CAT: the table model has no catalogs + valueInRow.add(null); + valueInRow.add(table.database()); + valueInRow.add(table.tableName()); + valueInRow.add(TABLE_TYPE_TABLE); + valueInRow.add(table.comment() == null ? "" : table.comment()); + valueInRow.add(getTypePrecision(fields[5].getSqlType())); + valueInRow.add(getTypeScale(fields[6].getSqlType())); valuesList.add(valueInRow); } - // Convert Values to ByteBuffer - ByteBuffer tsBlock = null; - try { - tsBlock = convertTsBlock(valuesList, tsDataTypeList); - } catch (IOException e) { - LOGGER.error(CONVERT_ERROR_MSG, e.getMessage()); - } finally { - close(rs, stmt); - } - - return hasResultSet - ? new IoTDBJDBCResultSet( - stmt, - columnNameList, - columnTypeList, - columnNameIndex, - true, - client, - null, - -1, - sessionId, - Collections.singletonList(tsBlock), - null, - (long) 60 * 1000, - false, - zoneId) - : null; + return toResultSet(stmt, fields, tsDataTypeList, valuesList); } /** @@ -328,7 +270,15 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada * @return eg. information_schema */ public static String removeEscape(String pattern) { - return pattern.replaceAll("\\_", "_"); + StringBuilder name = new StringBuilder(pattern.length()); + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == SEARCH_STRING_ESCAPE && i + 1 < pattern.length()) { + c = pattern.charAt(++i); + } + name.append(c); + } + return name.toString(); } @Override @@ -337,30 +287,9 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada throws SQLException { Statement stmt = this.connection.createStatement(); - boolean legacyMode = true; - ResultSet rs; - - // Get Table Metadata + List<ColumnInfo> columns; try { - String sql = - String.format( - "select * from information_schema.columns where database like '%s' escape '\\' and table_name like '%s' escape '\\'", - schemaPattern, tableNamePattern); - rs = stmt.executeQuery(sql); - } catch (SQLException e) { - LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); - - try { - String sql = - String.format( - "desc %s.%s details", removeEscape(schemaPattern), removeEscape(tableNamePattern)); - rs = stmt.executeQuery(sql); - legacyMode = false; - } catch (SQLException e1) { - LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); - throw e; - } - + columns = listColumns(stmt, schemaPattern, tableNamePattern, columnNamePattern); } finally { stmt.close(); } @@ -418,36 +347,27 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada TSDataType.TEXT, TSDataType.TEXT, TSDataType.TEXT); - List<String> columnNameList = new ArrayList<>(); - List<String> columnTypeList = new ArrayList<>(); - Map<String, Integer> columnNameIndex = new HashMap<>(); List<List<Object>> valuesList = new ArrayList<>(); - for (int i = 0; i < fields.length; i++) { - columnNameList.add(fields[i].getName()); - columnTypeList.add(fields[i].getSqlType()); - columnNameIndex.put(fields[i].getName(), i); - } // Extract Metadata - int count = 1; - while (rs.next()) { - String columnName = - legacyMode ? rs.getString("column_name") : rs.getString("ColumnName"); // 3 - String type = legacyMode ? rs.getString("datatype") : rs.getString("DataType"); // 4 + for (ColumnInfo column : columns) { + // The time column can have any name, so it is recognized by its category + boolean isTimeColumn = TIME_CATEGORY.equals(column.category()); List<Object> valueInRow = new ArrayList<>(); for (int i = 0; i < fields.length; i++) { if (i == 0) { - valueInRow.add(""); + // TABLE_CAT: the table model has no catalogs + valueInRow.add(null); } else if (i == 1) { - valueInRow.add(schemaPattern); + valueInRow.add(column.database()); } else if (i == 2) { - valueInRow.add(tableNamePattern); + valueInRow.add(column.tableName()); } else if (i == 3) { - valueInRow.add(columnName); + valueInRow.add(column.columnName()); } else if (i == 4) { - valueInRow.add(getSQLType(type)); + valueInRow.add(getSQLType(column.dataType())); } else if (i == 5) { - valueInRow.add(type); + valueInRow.add(column.dataType()); } else if (i == 6) { valueInRow.add(0); } else if (i == 7) { @@ -457,13 +377,13 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada } else if (i == 9) { valueInRow.add(0); } else if (i == 10) { - if (!columnName.equals("time")) { + if (!isTimeColumn) { valueInRow.add(ResultSetMetaData.columnNullableUnknown); } else { valueInRow.add(ResultSetMetaData.columnNoNulls); } } else if (i == 11) { - String comment = legacyMode ? rs.getString("comment") : rs.getString("Comment"); + String comment = column.comment(); if (comment != null && !comment.isEmpty()) { valueInRow.add(comment); } else { @@ -478,9 +398,9 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada } else if (i == 15) { valueInRow.add(65535); } else if (i == 16) { - valueInRow.add(count++); + valueInRow.add(column.ordinalPosition()); } else if (i == 17) { - if (!columnName.equals("time")) { + if (!isTimeColumn) { valueInRow.add("YES"); } else { valueInRow.add("NO"); @@ -504,14 +424,167 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada valuesList.add(valueInRow); } - // Convert Values to ByteBuffer - ByteBuffer tsBlock = null; + return toResultSet(stmt, fields, tsDataTypeList, valuesList); + } + + /** Lists the tables matching the patterns, with their real database and table names. */ + private List<TableInfo> listTables(Statement stmt, String schemaPattern, String tableNamePattern) + throws SQLException { + String sql = informationSchemaQuery("tables", schemaPattern, tableNamePattern); + try (ResultSet rs = stmt.executeQuery(sql)) { + List<TableInfo> tables = new ArrayList<>(); + while (rs.next()) { + tables.add( + new TableInfo( + rs.getString("database"), rs.getString("table_name"), rs.getString("comment"))); + } + return tables; + } catch (SQLException e) { + LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); + try { + return listTablesWithShowStatements(stmt, schemaPattern, tableNamePattern); + } catch (SQLException fallbackException) { + e.addSuppressed(fallbackException); + throw e; + } + } + } + + /** + * Lists tables with SHOW statements, for users who may not query information_schema. SHOW TABLES + * cannot filter by pattern, so the patterns are matched here. + */ + private List<TableInfo> listTablesWithShowStatements( + Statement stmt, String schemaPattern, String tableNamePattern) throws SQLException { + List<String> databases = new ArrayList<>(); + if (isLiteral(schemaPattern)) { + databases.add(removeEscape(schemaPattern)); + } else { + try (ResultSet rs = stmt.executeQuery(SHOW_DATABASES_SQL)) { + while (rs.next()) { + String database = rs.getString(1); + if (matchesPattern(database, schemaPattern)) { + databases.add(database); + } + } + } + } + + List<TableInfo> tables = new ArrayList<>(); + for (String database : databases) { + try (ResultSet rs = + stmt.executeQuery("show tables details from " + quoteIdentifier(database))) { + while (rs.next()) { + String tableName = rs.getString("TableName"); + if (matchesPattern(tableName, tableNamePattern)) { + tables.add(new TableInfo(database, tableName, rs.getString("Comment"))); + } + } + } + } + return tables; + } + + /** + * Lists the columns matching the patterns. ORDINAL_POSITION counts every column of the table, so + * the column name pattern is matched here rather than by the server. + */ + private List<ColumnInfo> listColumns( + Statement stmt, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + String sql = informationSchemaQuery("columns", schemaPattern, tableNamePattern); + try (ResultSet rs = stmt.executeQuery(sql)) { + List<ColumnInfo> columns = new ArrayList<>(); + // information_schema lists the columns of each table in their defined order + Map<List<String>, Integer> ordinalPositions = new HashMap<>(); + while (rs.next()) { + String database = rs.getString("database"); + String tableName = rs.getString("table_name"); + int ordinalPosition = + ordinalPositions.merge(Arrays.asList(database, tableName), 1, Integer::sum); + String columnName = rs.getString("column_name"); + if (matchesPattern(columnName, columnNamePattern)) { + columns.add( + new ColumnInfo( + database, + tableName, + columnName, + rs.getString("datatype"), + rs.getString("category"), + rs.getString("comment"), + ordinalPosition)); + } + } + return columns; + } catch (SQLException e) { + LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage()); + try { + return listColumnsWithDescribe(stmt, schemaPattern, tableNamePattern, columnNamePattern); + } catch (SQLException fallbackException) { + e.addSuppressed(fallbackException); + throw e; + } + } + } + + /** Lists columns with DESC statements, for users who may not query information_schema. */ + private List<ColumnInfo> listColumnsWithDescribe( + Statement stmt, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + List<ColumnInfo> columns = new ArrayList<>(); + for (TableInfo table : listTablesWithShowStatements(stmt, schemaPattern, tableNamePattern)) { + String sql = + "desc " + + quoteIdentifier(table.database()) + + "." + + quoteIdentifier(table.tableName()) + + " details"; + try (ResultSet rs = stmt.executeQuery(sql)) { + int ordinalPosition = 0; + while (rs.next()) { + ordinalPosition++; + String columnName = rs.getString("ColumnName"); + if (matchesPattern(columnName, columnNamePattern)) { + columns.add( + new ColumnInfo( + table.database(), + table.tableName(), + columnName, + rs.getString("DataType"), + rs.getString("Category"), + rs.getString("Comment"), + ordinalPosition)); + } + } + } + } + return columns; + } + + private ResultSet toResultSet( + Statement stmt, + Field[] fields, + List<TSDataType> tsDataTypeList, + List<List<Object>> valuesList) + throws SQLException { + List<String> columnNameList = new ArrayList<>(); + List<String> columnTypeList = new ArrayList<>(); + Map<String, Integer> columnNameIndex = new HashMap<>(); + for (int i = 0; i < fields.length; i++) { + columnNameList.add(fields[i].getName()); + columnTypeList.add(fields[i].getSqlType()); + columnNameIndex.put(fields[i].getName(), i); + } + + // Convert Values to ByteBuffer. No rows must mean no TsBlock: an empty one reads as one row. + List<ByteBuffer> tsBlocks; try { - tsBlock = convertTsBlock(valuesList, tsDataTypeList); + tsBlocks = + valuesList.isEmpty() + ? Collections.emptyList() + : Collections.singletonList(convertTsBlock(valuesList, tsDataTypeList)); } catch (IOException e) { - LOGGER.error(CONVERT_ERROR_MSG, e.getMessage()); - } finally { - close(rs, stmt); + throw new SQLException(e); } return new IoTDBJDBCResultSet( @@ -524,13 +597,88 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada null, -1, sessionId, - Collections.singletonList(tsBlock), + tsBlocks, null, (long) 60 * 1000, false, zoneId); } + /** + * getTableTypes() reports the type as "table", so the requested types are compared ignoring case. + */ + private static boolean isTableTypeRequested(String[] types) { + if (types == null) { + return true; + } + for (String type : types) { + if (TABLE_TYPE_TABLE.equalsIgnoreCase(type)) { + return true; + } + } + return false; + } + + /** A query on information_schema.{@code table}, narrowed by the non-null name patterns. */ + private static String informationSchemaQuery( + String table, String schemaPattern, String tableNamePattern) { + StringBuilder sql = new StringBuilder("select * from information_schema.").append(table); + String keyword = " where "; + if (schemaPattern != null) { + sql.append(keyword).append(likePredicate("database", schemaPattern)); + keyword = " and "; + } + if (tableNamePattern != null) { + sql.append(keyword).append(likePredicate("table_name", tableNamePattern)); + } + return sql.toString(); + } + + private static String likePredicate(String column, String pattern) { + return column + " like '" + pattern.replace("'", "''") + "' escape '\\'"; + } + + /** Whether the pattern names a single object, i.e. it has no unescaped wildcard. */ + private static boolean isLiteral(String pattern) { + if (pattern == null) { + return false; + } + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == SEARCH_STRING_ESCAPE) { + i++; + } else if (c == '%' || c == '_') { + return false; + } + } + return true; + } + + /** Matches a name the way LIKE matches it; a null pattern matches every name. */ + private static boolean matchesPattern(String name, String pattern) { + if (pattern == null) { + return true; + } + StringBuilder regex = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == SEARCH_STRING_ESCAPE && i + 1 < pattern.length()) { + regex.append(Pattern.quote(String.valueOf(pattern.charAt(++i)))); + } else if (c == '%') { + regex.append(".*"); + } else if (c == '_') { + regex.append('.'); + } else { + regex.append(Pattern.quote(String.valueOf(c))); + } + } + return Pattern.compile(regex.toString(), Pattern.DOTALL).matcher(name).matches(); + } + + private static String quoteIdentifier(String name) { + return "\"" + name.replace("\"", "\"\"") + "\""; + } + @Override public ResultSet getPrimaryKeys(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { @@ -653,4 +801,17 @@ public class IoTDBRelationalDatabaseMetadata extends IoTDBAbstractDatabaseMetada public String getSchemaTerm() throws SQLException { return ""; } + + /** A table as listed by the server, with its real (unescaped) names. */ + private record TableInfo(String database, String tableName, String comment) {} + + /** A column as listed by the server, with its real (unescaped) names. */ + private record ColumnInfo( + String database, + String tableName, + String columnName, + String dataType, + String category, + String comment, + int ordinalPosition) {} } 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 5805fb10330..21be0d33e9e 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 @@ -30,9 +30,12 @@ import org.apache.iotdb.service.rpc.thrift.TSFetchMetadataResp; import org.apache.iotdb.service.rpc.thrift.TSFetchResultsReq; import org.apache.iotdb.service.rpc.thrift.TSFetchResultsResp; +import org.apache.tsfile.block.column.ColumnBuilder; +import org.apache.tsfile.common.conf.TSFileConfig; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.read.common.block.TsBlockBuilder; import org.apache.tsfile.read.common.block.column.TsBlockSerde; +import org.apache.tsfile.utils.Binary; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -47,11 +50,14 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; +import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -107,6 +113,18 @@ import static org.mockito.Mockito.when; public class IoTDBJDBCResultSetTest { + private static final List<TSDataType> ALL_TYPES = + Arrays.asList( + TSDataType.BOOLEAN, + TSDataType.INT32, + TSDataType.INT64, + TSDataType.FLOAT, + TSDataType.DOUBLE, + TSDataType.STRING, + TSDataType.BLOB, + TSDataType.TIMESTAMP, + TSDataType.DATE); + @Mock TSExecuteStatementResp execResp; private long queryId; private long sessionId; @@ -292,6 +310,146 @@ public class IoTDBJDBCResultSetTest { } } + @SuppressWarnings("resource") + @Test + public void testGetObjectConvertsToRequestedClass() throws Exception { + try (ResultSet resultSet = + executeTableModelQueryWithAllTypes( + 1_000_000, + new Object[] { + true, + 42, + 7L, + 1.5F, + 2.25D, + "hello", + new byte[] {1, 2, 3}, + 1_700_000_000_123_456L, + 20240115 + })) { + Assert.assertTrue(resultSet.next()); + Assert.assertEquals(Boolean.TRUE, resultSet.getObject(1, Boolean.class)); + Assert.assertEquals(Integer.valueOf(42), resultSet.getObject(2, Integer.class)); + Assert.assertEquals(Long.valueOf(42L), resultSet.getObject(2, Long.class)); + Assert.assertEquals(Long.valueOf(7L), resultSet.getObject(3, Long.class)); + Assert.assertEquals(Float.valueOf(1.5F), resultSet.getObject(4, Float.class)); + Assert.assertEquals(Double.valueOf(1.5D), resultSet.getObject(4, Double.class)); + Assert.assertEquals(Double.valueOf(2.25D), resultSet.getObject(5, Double.class)); + Assert.assertEquals("hello", resultSet.getObject(6, String.class)); + Assert.assertArrayEquals(new byte[] {1, 2, 3}, resultSet.getObject(7, byte[].class)); + Timestamp expectedTimestamp = new Timestamp(1_700_000_000_123L); + expectedTimestamp.setNanos(123_456_000); + Assert.assertEquals(expectedTimestamp, resultSet.getObject(8, Timestamp.class)); + Assert.assertEquals(Long.valueOf(1_700_000_000_123_456L), resultSet.getObject(8, Long.class)); + Assert.assertEquals(LocalDate.of(2024, 1, 15), resultSet.getObject(9, LocalDate.class)); + Assert.assertEquals("42", resultSet.getObject(2, String.class)); + } + } + + @SuppressWarnings("resource") + @Test + public void testGetObjectReturnsNullForSqlNull() throws Exception { + try (ResultSet resultSet = + executeTableModelQueryWithAllTypes(1_000, new Object[ALL_TYPES.size()])) { + Assert.assertTrue(resultSet.next()); + Assert.assertNull(resultSet.getObject(1, Boolean.class)); + Assert.assertNull(resultSet.getObject(2, Integer.class)); + Assert.assertNull(resultSet.getObject(3, Long.class)); + Assert.assertNull(resultSet.getObject(4, Float.class)); + Assert.assertNull(resultSet.getObject(5, Double.class)); + Assert.assertNull(resultSet.getObject(6, String.class)); + Assert.assertNull(resultSet.getObject(7, byte[].class)); + Assert.assertNull(resultSet.getObject(8, Timestamp.class)); + Assert.assertNull(resultSet.getObject(9, LocalDate.class)); + Assert.assertTrue(resultSet.wasNull()); + } + } + + @SuppressWarnings("resource") + @Test + public void testGetObjectRejectsUnsupportedConversion() throws Exception { + try (ResultSet resultSet = + executeTableModelQueryWithAllTypes( + 1_000, new Object[] {true, 42, 7L, 1.5F, 2.25D, "hello", new byte[0], 0L, 20240115})) { + Assert.assertTrue(resultSet.next()); + assertGetObjectRejected(() -> resultSet.getObject(6, Integer.class)); + assertGetObjectRejected(() -> resultSet.getObject(1, Long.class)); + assertGetObjectRejected(() -> resultSet.getObject(9, Timestamp.class)); + assertGetObjectRejected(() -> resultSet.getObject(2, StringBuilder.class)); + assertGetObjectRejected(() -> resultSet.getObject(2, (Class<?>) null)); + } + } + + private void assertGetObjectRejected(ResultSetRead read) { + try { + read.run(); + Assert.fail("the conversion should have been rejected"); + } catch (SQLException e) { + // expected + } catch (RuntimeException e) { + Assert.fail( + "expected a SQLException but got " + e.getClass().getName() + ": " + e.getMessage()); + } + } + + /** Runs a table-model query whose single row holds one column of each type in ALL_TYPES. */ + private ResultSet executeTableModelQueryWithAllTypes(int timeFactor, Object[] row) + throws Exception { + TsBlockBuilder tsBlockBuilder = new TsBlockBuilder(ALL_TYPES); + tsBlockBuilder.getTimeColumnBuilder().writeLong(0); + for (int i = 0; i < ALL_TYPES.size(); i++) { + ColumnBuilder columnBuilder = tsBlockBuilder.getColumnBuilder(i); + Object value = row[i]; + if (value == null) { + columnBuilder.appendNull(); + continue; + } + switch (ALL_TYPES.get(i)) { + case BOOLEAN: + columnBuilder.writeBoolean((Boolean) value); + break; + case INT32: + case DATE: + columnBuilder.writeInt((Integer) value); + break; + case INT64: + case TIMESTAMP: + columnBuilder.writeLong((Long) value); + break; + case FLOAT: + columnBuilder.writeFloat((Float) value); + break; + case DOUBLE: + columnBuilder.writeDouble((Double) value); + break; + case STRING: + columnBuilder.writeBinary(new Binary((String) value, TSFileConfig.STRING_CHARSET)); + break; + default: + columnBuilder.writeBinary(new Binary((byte[]) value)); + break; + } + } + tsBlockBuilder.declarePosition(); + + List<String> columns = Arrays.asList("b", "i", "l", "f", "d", "s", "blob", "ts", "dt"); + TSExecuteStatementResp resp = new TSExecuteStatementResp(successStatus); + resp.setQueryId(queryId); + resp.setColumns(columns); + resp.setDataTypeList(ALL_TYPES.stream().map(TSDataType::name).collect(Collectors.toList())); + resp.setIgnoreTimeStamp(true); + resp.setTableModel(true); + resp.setMoreData(false); + resp.setColumnIndex2TsBlockColumnIndexList( + IntStream.range(0, columns.size()).boxed().collect(Collectors.toList())); + resp.setQueryResult( + Collections.singletonList(new TsBlockSerde().serialize(tsBlockBuilder.build()))); + + when(connection.getTimeFactor()).thenReturn(timeFactor); + when(client.executeQueryStatementV2(any(TSExecuteStatementReq.class))).thenReturn(resp); + return statement.executeQuery("select * from all_types"); + } + @FunctionalInterface private interface ResultSetRead { void run() throws SQLException; diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBRelationalDatabaseMetadataTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBRelationalDatabaseMetadataTest.java new file mode 100644 index 00000000000..8bbc3d015b9 --- /dev/null +++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBRelationalDatabaseMetadataTest.java @@ -0,0 +1,539 @@ +/* + * 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.jdbc; + +import org.apache.iotdb.jdbc.relational.IoTDBRelationalDatabaseMetadata; +import org.apache.iotdb.rpc.RpcUtils; +import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.service.rpc.thrift.IClientRPCService; +import org.apache.iotdb.service.rpc.thrift.TSCloseOperationReq; +import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementReq; +import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementResp; + +import org.apache.tsfile.block.column.ColumnBuilder; +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.read.common.block.TsBlockBuilder; +import org.apache.tsfile.read.common.block.column.TsBlockSerde; +import org.apache.tsfile.utils.Binary; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Types; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class IoTDBRelationalDatabaseMetadataTest { + + private static final String TABLES_QUERY = "select * from information_schema.tables"; + private static final String COLUMNS_QUERY = "select * from information_schema.columns"; + + // Result headers as the server produces them for each metadata source. + private static final List<String> INFORMATION_SCHEMA_TABLES_HEADER = + Arrays.asList( + "database", + "table_name", + "ttl(ms)", + "status", + "comment", + "table_type", + "need_last_cache"); + private static final List<TSDataType> INFORMATION_SCHEMA_TABLES_TYPES = + Arrays.asList( + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.BOOLEAN); + private static final List<String> INFORMATION_SCHEMA_COLUMNS_HEADER = + Arrays.asList( + "database", "table_name", "column_name", "datatype", "category", "status", "comment"); + private static final List<String> SHOW_DATABASES_HEADER = + Arrays.asList( + "Database", + "TTL(ms)", + "SchemaReplicationFactor", + "DataReplicationFactor", + "TimePartitionInterval"); + private static final List<TSDataType> SHOW_DATABASES_TYPES = + Arrays.asList( + TSDataType.TEXT, TSDataType.TEXT, TSDataType.INT32, TSDataType.INT32, TSDataType.INT64); + private static final List<String> SHOW_TABLES_DETAILS_HEADER = + Arrays.asList("TableName", "TTL(ms)", "Status", "Comment", "TableType", "NeedLastCache"); + private static final List<TSDataType> SHOW_TABLES_DETAILS_TYPES = + Arrays.asList( + TSDataType.TEXT, + TSDataType.TEXT, + TSDataType.TEXT, + TSDataType.TEXT, + TSDataType.TEXT, + TSDataType.BOOLEAN); + private static final List<String> DESC_DETAILS_HEADER = + Arrays.asList("ColumnName", "DataType", "Category", "Status", "Comment"); + + @Mock private IoTDBConnection connection; + @Mock private IClientRPCService.Iface client; + + private final long sessionId = 1L; + private final ZoneId zoneId = ZoneId.systemDefault(); + private DatabaseMetaData metadata; + + /** Canned server responses, picked by the prefix of the statement the driver sends. */ + private final Map<String, TSExecuteStatementResp> responses = new LinkedHashMap<>(); + + private final List<String> executedStatements = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(connection.isClosed()).thenReturn(false); + when(connection.getTimeFactor()).thenReturn(1_000); + when(connection.createStatement()) + .thenAnswer(invocation -> new IoTDBStatement(connection, client, sessionId, zoneId, 0, 1L)); + when(client.closeOperation(any(TSCloseOperationReq.class))).thenReturn(RpcUtils.SUCCESS_STATUS); + when(client.executeQueryStatementV2(any(TSExecuteStatementReq.class))) + .thenAnswer( + invocation -> { + String statement = ((TSExecuteStatementReq) invocation.getArgument(0)).getStatement(); + executedStatements.add(statement); + return responses.entrySet().stream() + .filter(entry -> statement.startsWith(entry.getKey())) + .map(entry -> withUnreadBuffers(entry.getValue())) + .findFirst() + .orElseGet(() -> failure("unexpected statement: " + statement)); + }); + metadata = new IoTDBRelationalDatabaseMetadata(connection, client, sessionId, zoneId); + } + + // ========== getTables ========== + + @Test + public void testGetTablesReportsRealNamesForEscapedPatterns() throws Exception { + responses.put( + TABLES_QUERY, + informationSchemaTables(informationSchemaTable("db_1", "sensor_data", "sensor table"))); + + try (ResultSet rs = metadata.getTables("Apache IoTDB", "db\\_1", "sensor\\_data", null)) { + assertTrue(rs.next()); + assertNull(rs.getString("TABLE_CAT")); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + assertEquals("TABLE", rs.getString("TABLE_TYPE")); + assertEquals("sensor table", rs.getString("REMARKS")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetTablesStartsWithStandardColumns() throws Exception { + responses.put( + TABLES_QUERY, informationSchemaTables(informationSchemaTable("db_1", "sensor_data", null))); + + try (ResultSet rs = metadata.getTables(null, "db_1", null, null)) { + assertEquals(1, rs.findColumn("TABLE_CAT")); + assertEquals(2, rs.findColumn("TABLE_SCHEM")); + assertEquals(3, rs.findColumn("TABLE_NAME")); + assertEquals(4, rs.findColumn("TABLE_TYPE")); + assertEquals(5, rs.findColumn("REMARKS")); + assertTrue(rs.next()); + assertEquals("db_1", rs.getString(2)); + assertEquals("sensor_data", rs.getString(3)); + } + } + + @Test + public void testGetTablesFiltersByTableNamePatternAndEscapesQuotes() throws Exception { + responses.put(TABLES_QUERY, informationSchemaTables()); + + metadata.getTables(null, "db\\_1", "it's%", null).close(); + + assertEquals( + Collections.singletonList( + "select * from information_schema.tables" + + " where database like 'db\\_1' escape '\\'" + + " and table_name like 'it''s%' escape '\\'"), + executedStatements); + } + + @Test + public void testGetTablesWithNullPatternsListsEveryDatabase() throws Exception { + responses.put( + TABLES_QUERY, + informationSchemaTables( + informationSchemaTable("db_1", "t1", null), + informationSchemaTable("db_2", "t2", null))); + + try (ResultSet rs = metadata.getTables(null, null, null, null)) { + assertTrue(rs.next()); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("t1", rs.getString("TABLE_NAME")); + assertTrue(rs.next()); + assertEquals("db_2", rs.getString("TABLE_SCHEM")); + assertEquals("t2", rs.getString("TABLE_NAME")); + assertFalse(rs.next()); + } + assertEquals( + Collections.singletonList("select * from information_schema.tables"), executedStatements); + } + + @Test + public void testGetTablesReturnsEmptyResultSetWhenNothingMatches() throws Exception { + responses.put(TABLES_QUERY, informationSchemaTables()); + + try (ResultSet rs = metadata.getTables(null, "no_such_db", null, null)) { + assertNotNull(rs); + assertEquals(3, rs.findColumn("TABLE_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetTablesHonorsTableTypes() throws Exception { + responses.put( + TABLES_QUERY, informationSchemaTables(informationSchemaTable("db_1", "sensor_data", null))); + + try (ResultSet rs = metadata.getTables(null, "db_1", null, new String[] {"VIEW"})) { + assertFalse(rs.next()); + } + // getTableTypes() reports the type in lower case, so the match must ignore case. + try (ResultSet rs = metadata.getTables(null, "db_1", null, new String[] {"table"})) { + assertTrue(rs.next()); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + } + try (ResultSet rs = metadata.getTables(null, "db_1", null, new String[] {"TABLE", "VIEW"})) { + assertTrue(rs.next()); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + } + } + + @Test + public void testGetTablesFallsBackToShowTablesForLiteralSchema() throws Exception { + responses.put(TABLES_QUERY, failure("no permission on information_schema")); + responses.put( + "show tables details from \"db_1\"", + showTablesDetails( + showTablesRow("sensor_data", "sensor table"), showTablesRow("other", ""))); + + try (ResultSet rs = metadata.getTables(null, "db\\_1", "sensor%", null)) { + assertTrue(rs.next()); + assertNull(rs.getString("TABLE_CAT")); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + assertEquals("sensor table", rs.getString("REMARKS")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetTablesFallbackListsEveryDatabaseForNullSchema() throws Exception { + responses.put(TABLES_QUERY, failure("no permission on information_schema")); + responses.put("SHOW DATABASES", showDatabases("db_1", "db_2")); + responses.put("show tables details from \"db_1\"", showTablesDetails(showTablesRow("t1", ""))); + responses.put("show tables details from \"db_2\"", showTablesDetails(showTablesRow("t2", ""))); + + try (ResultSet rs = metadata.getTables(null, null, null, null)) { + assertTrue(rs.next()); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("t1", rs.getString("TABLE_NAME")); + assertTrue(rs.next()); + assertEquals("db_2", rs.getString("TABLE_SCHEM")); + assertEquals("t2", rs.getString("TABLE_NAME")); + assertFalse(rs.next()); + } + } + + @Test + public void testRemoveEscapeDropsSearchStringEscapes() { + assertEquals( + "information_schema", IoTDBRelationalDatabaseMetadata.removeEscape("information\\_schema")); + assertEquals("a%b\\c", IoTDBRelationalDatabaseMetadata.removeEscape("a\\%b\\\\c")); + assertEquals("plain", IoTDBRelationalDatabaseMetadata.removeEscape("plain")); + } + + // ========== getColumns ========== + + @Test + public void testGetColumnsReportsRealNamesForEscapedPatterns() throws Exception { + responses.put( + COLUMNS_QUERY, + informationSchemaColumns( + informationSchemaColumn("db_1", "sensor_data", "ts", "TIMESTAMP", "TIME"), + informationSchemaColumn("db_1", "sensor_data", "region", "STRING", "TAG"))); + + try (ResultSet rs = metadata.getColumns(null, "db\\_1", "sensor\\_data", null)) { + assertTrue(rs.next()); + assertNull(rs.getString("TABLE_CAT")); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + assertEquals("ts", rs.getString("COLUMN_NAME")); + assertEquals(Types.TIMESTAMP, rs.getInt("DATA_TYPE")); + assertEquals("TIMESTAMP", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertNull(rs.getString("TABLE_CAT")); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + assertEquals("region", rs.getString("COLUMN_NAME")); + assertEquals(Types.VARCHAR, rs.getInt("DATA_TYPE")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetColumnsDerivesNullabilityFromTimeCategory() throws Exception { + responses.put( + COLUMNS_QUERY, + informationSchemaColumns( + informationSchemaColumn("db_1", "sensor_data", "ts", "TIMESTAMP", "TIME"), + informationSchemaColumn("db_1", "sensor_data", "temperature", "FLOAT", "FIELD"))); + + try (ResultSet rs = metadata.getColumns(null, "db_1", "sensor_data", null)) { + assertTrue(rs.next()); + assertEquals("ts", rs.getString("COLUMN_NAME")); + assertEquals(ResultSetMetaData.columnNoNulls, rs.getInt("NULLABLE")); + assertEquals("NO", rs.getString("IS_NULLABLE")); + assertTrue(rs.next()); + assertEquals("temperature", rs.getString("COLUMN_NAME")); + assertEquals(ResultSetMetaData.columnNullableUnknown, rs.getInt("NULLABLE")); + assertEquals("YES", rs.getString("IS_NULLABLE")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetColumnsNumbersColumnsWithinEachTable() throws Exception { + responses.put( + COLUMNS_QUERY, + informationSchemaColumns( + informationSchemaColumn("db_1", "t1", "time", "TIMESTAMP", "TIME"), + informationSchemaColumn("db_1", "t1", "a", "INT32", "FIELD"), + informationSchemaColumn("db_1", "t1", "b", "INT64", "FIELD"), + informationSchemaColumn("db_1", "t2", "time", "TIMESTAMP", "TIME"), + informationSchemaColumn("db_1", "t2", "c", "DOUBLE", "FIELD"))); + + List<String> columns = new ArrayList<>(); + try (ResultSet rs = metadata.getColumns(null, "db_1", "t%", null)) { + while (rs.next()) { + columns.add( + rs.getString("TABLE_NAME") + + "." + + rs.getString("COLUMN_NAME") + + "#" + + rs.getInt("ORDINAL_POSITION")); + } + } + assertEquals(Arrays.asList("t1.time#1", "t1.a#2", "t1.b#3", "t2.time#1", "t2.c#2"), columns); + } + + @Test + public void testGetColumnsFiltersByColumnNamePatternKeepingOrdinalPosition() throws Exception { + responses.put( + COLUMNS_QUERY, + informationSchemaColumns( + informationSchemaColumn("db_1", "t1", "time", "TIMESTAMP", "TIME"), + informationSchemaColumn("db_1", "t1", "a", "INT32", "FIELD"), + informationSchemaColumn("db_1", "t1", "b", "INT64", "FIELD"))); + + try (ResultSet rs = metadata.getColumns(null, "db_1", "t1", "b")) { + assertTrue(rs.next()); + assertEquals("b", rs.getString("COLUMN_NAME")); + assertEquals(3, rs.getInt("ORDINAL_POSITION")); + assertFalse(rs.next()); + } + } + + @Test + public void testGetColumnsBuildsNullSafeQuotedQuery() throws Exception { + responses.put(COLUMNS_QUERY, informationSchemaColumns()); + + metadata.getColumns(null, "db\\_1", "it's", "c%").close(); + metadata.getColumns(null, null, null, null).close(); + + assertEquals( + Arrays.asList( + "select * from information_schema.columns" + + " where database like 'db\\_1' escape '\\'" + + " and table_name like 'it''s' escape '\\'", + "select * from information_schema.columns"), + executedStatements); + } + + @Test + public void testGetColumnsReturnsEmptyResultSetForUnknownTable() throws Exception { + responses.put(COLUMNS_QUERY, informationSchemaColumns()); + + try (ResultSet rs = metadata.getColumns(null, "db_1", "no_such_table", null)) { + assertFalse(rs.next()); + } + } + + @Test + public void testGetColumnsFallsBackToDescribeTable() throws Exception { + responses.put(COLUMNS_QUERY, failure("no permission on information_schema")); + responses.put( + "show tables details from \"db_1\"", + showTablesDetails(showTablesRow("sensor_data", ""), showTablesRow("sensorXdata", ""))); + responses.put( + "desc \"db_1\".\"sensor_data\" details", + describeTableDetails( + new Object[] {"ts", "TIMESTAMP", "TIME", "USING", null}, + new Object[] {"temperature", "FLOAT", "FIELD", "USING", "celsius"})); + + try (ResultSet rs = metadata.getColumns(null, "db\\_1", "sensor\\_data", null)) { + assertTrue(rs.next()); + assertNull(rs.getString("TABLE_CAT")); + assertEquals("db_1", rs.getString("TABLE_SCHEM")); + assertEquals("sensor_data", rs.getString("TABLE_NAME")); + assertEquals("ts", rs.getString("COLUMN_NAME")); + assertEquals(ResultSetMetaData.columnNoNulls, rs.getInt("NULLABLE")); + assertEquals(1, rs.getInt("ORDINAL_POSITION")); + assertTrue(rs.next()); + assertEquals("temperature", rs.getString("COLUMN_NAME")); + assertEquals("celsius", rs.getString("REMARKS")); + assertEquals("YES", rs.getString("IS_NULLABLE")); + assertEquals(2, rs.getInt("ORDINAL_POSITION")); + assertFalse(rs.next()); + } + } + + // ========== fake server responses ========== + + private static Object[] informationSchemaTable(String database, String table, String comment) { + return new Object[] {database, table, "INF", "USING", comment, "BASE TABLE", false}; + } + + private static TSExecuteStatementResp informationSchemaTables(Object[]... rows) + throws IOException { + return queryResult(INFORMATION_SCHEMA_TABLES_HEADER, INFORMATION_SCHEMA_TABLES_TYPES, rows); + } + + private static Object[] informationSchemaColumn( + String database, String table, String column, String dataType, String category) { + return new Object[] {database, table, column, dataType, category, "USING", null}; + } + + private static TSExecuteStatementResp informationSchemaColumns(Object[]... rows) + throws IOException { + return queryResult( + INFORMATION_SCHEMA_COLUMNS_HEADER, + Collections.nCopies(INFORMATION_SCHEMA_COLUMNS_HEADER.size(), TSDataType.STRING), + rows); + } + + private static TSExecuteStatementResp showDatabases(String... databases) throws IOException { + Object[][] rows = new Object[databases.length][]; + for (int i = 0; i < databases.length; i++) { + rows[i] = new Object[] {databases[i], "INF", 1, 1, 604_800_000L}; + } + return queryResult(SHOW_DATABASES_HEADER, SHOW_DATABASES_TYPES, rows); + } + + private static Object[] showTablesRow(String table, String comment) { + return new Object[] {table, "INF", "USING", comment, "BASE TABLE", false}; + } + + private static TSExecuteStatementResp showTablesDetails(Object[]... rows) throws IOException { + return queryResult(SHOW_TABLES_DETAILS_HEADER, SHOW_TABLES_DETAILS_TYPES, rows); + } + + private static TSExecuteStatementResp describeTableDetails(Object[]... rows) throws IOException { + return queryResult( + DESC_DETAILS_HEADER, + Collections.nCopies(DESC_DETAILS_HEADER.size(), TSDataType.TEXT), + rows); + } + + /** A successful table-model query response carrying the given rows in one TsBlock. */ + private static TSExecuteStatementResp queryResult( + List<String> header, List<TSDataType> types, Object[]... rows) throws IOException { + TsBlockBuilder builder = new TsBlockBuilder(types); + for (Object[] row : rows) { + builder.getTimeColumnBuilder().writeLong(0); + for (int i = 0; i < row.length; i++) { + ColumnBuilder columnBuilder = builder.getColumnBuilder(i); + Object value = row[i]; + if (value == null) { + columnBuilder.appendNull(); + } else if (value instanceof Boolean) { + columnBuilder.writeBoolean((Boolean) value); + } else if (value instanceof Integer) { + columnBuilder.writeInt((Integer) value); + } else if (value instanceof Long) { + columnBuilder.writeLong((Long) value); + } else { + columnBuilder.writeBinary(new Binary((String) value, TSFileConfig.STRING_CHARSET)); + } + } + builder.declarePosition(); + } + + TSExecuteStatementResp resp = new TSExecuteStatementResp(RpcUtils.SUCCESS_STATUS); + resp.setQueryId(100L); + resp.setColumns(header); + resp.setDataTypeList(types.stream().map(TSDataType::name).collect(Collectors.toList())); + resp.setIgnoreTimeStamp(true); + resp.setTableModel(true); + resp.setMoreData(false); + resp.setColumnIndex2TsBlockColumnIndexList( + IntStream.range(0, header.size()).boxed().collect(Collectors.toList())); + resp.setQueryResult( + rows.length == 0 + ? Collections.emptyList() + : Collections.singletonList(new TsBlockSerde().serialize(builder.build()))); + return resp; + } + + /** Reading a result consumes its TsBlock buffers, so every execution gets its own view. */ + private static TSExecuteStatementResp withUnreadBuffers(TSExecuteStatementResp resp) { + TSExecuteStatementResp copy = resp.deepCopy(); + if (copy.isSetQueryResult()) { + copy.setQueryResult( + copy.getQueryResult().stream().map(ByteBuffer::duplicate).collect(Collectors.toList())); + } + return copy; + } + + private static TSExecuteStatementResp failure(String message) { + return RpcUtils.getTSExecuteStatementResp(TSStatusCode.NO_PERMISSION, message); + } +} diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatementTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatementTest.java index dd1c7ecbcb2..83e91ddddeb 100644 --- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatementTest.java +++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBTablePreparedStatementTest.java @@ -20,9 +20,13 @@ package org.apache.iotdb.jdbc; import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.rpc.RpcUtils; import org.apache.iotdb.rpc.TSStatusCode; +import org.apache.iotdb.rpc.stmt.PreparedParameterSerde; import org.apache.iotdb.service.rpc.thrift.IClientRPCService.Iface; +import org.apache.iotdb.service.rpc.thrift.TSCancelOperationReq; import org.apache.iotdb.service.rpc.thrift.TSExecutePreparedReq; +import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementReq; import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementResp; import org.apache.iotdb.service.rpc.thrift.TSPrepareReq; import org.apache.iotdb.service.rpc.thrift.TSPrepareResp; @@ -35,10 +39,20 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.nio.ByteBuffer; +import java.sql.SQLException; +import java.sql.Timestamp; import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -237,4 +251,145 @@ public class IoTDBTablePreparedStatementTest { verify(client).executePreparedStatement(argument.capture()); assertTrue(argument.getValue().getParameters() != null); } + + // ========== Cancel, fetch size and timestamp precision ========== + + @SuppressWarnings("resource") + @Test + public void testCancelTargetsQueryOfLatestPreparedExecution() throws Exception { + when(client.executePreparedStatement(any(TSExecutePreparedReq.class))) + .thenReturn(preparedQueryResp(42L), preparedQueryResp(43L)); + when(client.cancelOperation(any(TSCancelOperationReq.class))) + .thenReturn(RpcUtils.SUCCESS_STATUS); + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "SELECT * FROM t WHERE s1 = ?", zoneId); + ps.setInt(1, 1); + + ps.execute(); + ps.cancel(); + ps.execute(); + ps.cancel(); + + ArgumentCaptor<TSCancelOperationReq> argument = + ArgumentCaptor.forClass(TSCancelOperationReq.class); + verify(client, times(2)).cancelOperation(argument.capture()); + assertEquals( + Arrays.asList(42L, 43L), + argument.getAllValues().stream() + .map(TSCancelOperationReq::getQueryId) + .collect(Collectors.toList())); + assertEquals(sessionId, argument.getValue().getSessionId()); + } + + @SuppressWarnings("resource") + @Test + public void testPreparedExecutionSendsFetchSize() throws Exception { + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "SELECT * FROM t WHERE s1 = ?", zoneId); + ps.setInt(1, 1); + ps.setFetchSize(123); + + ps.execute(); + + TSExecutePreparedReq req = lastPreparedExecution(); + assertTrue(req.isSetFetchSize()); + assertEquals(123, req.getFetchSize()); + } + + @SuppressWarnings("resource") + @Test + public void testPreparedExecutionFetchSizeIsCappedByMaxRows() throws Exception { + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "SELECT * FROM t WHERE s1 = ?", zoneId); + ps.setInt(1, 1); + ps.setFetchSize(1000); + ps.setMaxRows(10); + + ps.execute(); + + assertEquals(10, lastPreparedExecution().getFetchSize()); + } + + @Test + public void testSetTimestampUsesServerTimePrecision() throws Exception { + Timestamp timestamp = new Timestamp(1_700_000_000_123L); + timestamp.setNanos(123_456_789); + assertEquals(1_700_000_000_123L, preparedTimestampValue(1_000, timestamp)); + assertEquals(1_700_000_000_123_456L, preparedTimestampValue(1_000_000, timestamp)); + assertEquals(1_700_000_000_123_456_789L, preparedTimestampValue(1_000_000_000, timestamp)); + + // 1.5 seconds before the epoch: the whole-second part must be floored, not truncated. + Timestamp beforeEpoch = new Timestamp(-1_500L); + assertEquals(-1_500L, preparedTimestampValue(1_000, beforeEpoch)); + assertEquals(-1_500_000L, preparedTimestampValue(1_000_000, beforeEpoch)); + assertEquals(-1_500_000_000L, preparedTimestampValue(1_000_000_000, beforeEpoch)); + } + + @SuppressWarnings("resource") + @Test + public void testSetTimestampRejectsValueBeyondServerPrecisionRange() throws Exception { + when(connection.getTimeFactor()).thenReturn(1_000_000_000); + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "SELECT * FROM t WHERE time = ?", zoneId); + + // Nanosecond precision can only represent instants up to the year 2262. + Timestamp farFuture = Timestamp.valueOf("2300-01-01 00:00:00"); + assertThrows(SQLException.class, () -> ps.setTimestamp(1, farFuture)); + } + + @SuppressWarnings("resource") + @Test + public void testSetTimestampInNonQueryStatementUsesServerTimePrecision() throws Exception { + when(connection.getTimeFactor()).thenReturn(1_000_000); + when(client.executeUpdateStatement(any(TSExecuteStatementReq.class))) + .thenReturn(new TSExecuteStatementResp(RpcUtils.SUCCESS_STATUS)); + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "INSERT INTO t(time, s1) VALUES(?, 1)", zoneId); + Timestamp timestamp = new Timestamp(1_700_000_000_123L); + timestamp.setNanos(123_456_789); + ps.setTimestamp(1, timestamp); + + ps.executeUpdate(); + + ArgumentCaptor<TSExecuteStatementReq> argument = + ArgumentCaptor.forClass(TSExecuteStatementReq.class); + verify(client).executeUpdateStatement(argument.capture()); + assertEquals( + "INSERT INTO t(time, s1) VALUES(1700000000123456, 1)", argument.getValue().getStatement()); + } + + private static TSExecuteStatementResp preparedQueryResp(long queryId) { + TSExecuteStatementResp resp = new TSExecuteStatementResp(RpcUtils.SUCCESS_STATUS); + resp.setQueryId(queryId); + return resp; + } + + private TSExecutePreparedReq lastPreparedExecution() throws Exception { + ArgumentCaptor<TSExecutePreparedReq> argument = + ArgumentCaptor.forClass(TSExecutePreparedReq.class); + verify(client, atLeastOnce()).executePreparedStatement(argument.capture()); + return argument.getValue(); + } + + /** Binds {@code timestamp} under the given server time factor and returns the value sent. */ + @SuppressWarnings("resource") + private long preparedTimestampValue(int timeFactor, Timestamp timestamp) throws Exception { + when(connection.getTimeFactor()).thenReturn(timeFactor); + IoTDBTablePreparedStatement ps = + new IoTDBTablePreparedStatement( + connection, client, sessionId, "SELECT * FROM t WHERE time = ?", zoneId); + ps.setTimestamp(1, timestamp); + ps.execute(); + + List<PreparedParameterSerde.DeserializedParam> parameters = + PreparedParameterSerde.deserialize( + ByteBuffer.wrap(lastPreparedExecution().getParameters())); + assertEquals(1, parameters.size()); + return (Long) parameters.get(0).value; + } }
