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;
+  }
 }

Reply via email to