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 c0dcfbe0d49f07385d704df2a18aafafe488baed
Author: JackieTien97 <[email protected]>
AuthorDate: Wed Oct 21 14:28:07 2026 +0800

    [JDBC] Fix getPrimaryKeys names, NULL getTimestamp and getObject by label
    
    - getPrimaryKeys reports the real database and table names with a NULL
      TABLE_CAT, adds no filter for a null schema or table, escapes quotes,
      matches the given names exactly whether or not they are escaped, numbers
      KEY_SEQ within each table and returns an empty result set when nothing
      matches. It now shares the column listing (and SHOW / DESC fallback)
      with getColumns.
    - getTimestamp returns null for SQL NULL instead of 1970-01-01, and the
      by-label variant now honors the time precision and reports an unknown
      label as a SQLException.
    - Implement getObject(String, Class) on top of getObject(int, Class).
---
 .../org/apache/iotdb/jdbc/i18n/JdbcMessages.java   |   1 +
 .../org/apache/iotdb/jdbc/i18n/JdbcMessages.java   |   1 +
 .../org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java  |  28 ++++--
 .../IoTDBRelationalDatabaseMetadata.java           | 102 ++++++---------------
 .../apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java  |  83 ++++++++++++-----
 .../jdbc/IoTDBRelationalDatabaseMetadataTest.java  |  81 ++++++++++++++++
 6 files changed, 195 insertions(+), 101 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 ab0206f33ed..fe2d071b066 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
@@ -229,5 +229,6 @@ public final class JdbcMessages {
   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: {}";
+  public static final String EXCEPTION_UNKNOWN_COLUMN_NAME_ARG_F8369278 = 
"Unknown column name: %s";
 
 }
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 35efaac0944..ccbfc04a02f 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
@@ -228,5 +228,6 @@ public final class JdbcMessages {
   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 = "查询表信息出错:{}";
+  public static final String EXCEPTION_UNKNOWN_COLUMN_NAME_ARG_F8369278 = 
"未知列名:%s";
 
 }
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 138d22d56bd..7857e624a87 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
@@ -68,8 +68,6 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
-import static org.apache.iotdb.rpc.RpcUtils.convertToTimestamp;
-
 public class IoTDBJDBCResultSet implements ResultSet {
 
   public static final String OBJECT_ERR_MSG = "OBJECT Type only support 
getString";
@@ -697,8 +695,18 @@ public class IoTDBJDBCResultSet implements ResultSet {
   }
 
   @Override
-  public <T> T getObject(String arg0, Class<T> arg1) throws SQLException {
-    throw new SQLException(Constant.METHOD_NOT_SUPPORTED);
+  public <T> T getObject(String columnLabel, Class<T> type) throws 
SQLException {
+    return getObject(findColumnIndex(columnLabel), type);
+  }
+
+  // findColumn() fails with an unchecked exception for an unknown label
+  private int findColumnIndex(String columnLabel) throws SQLException {
+    int index = ioTDBRpcDataSet.getColumnNameList().indexOf(columnLabel);
+    if (index < 0) {
+      throw new SQLException(
+          
String.format(JdbcMessages.EXCEPTION_UNKNOWN_COLUMN_NAME_ARG_F8369278, 
columnLabel));
+    }
+    return index + 1;
   }
 
   @Override
@@ -788,12 +796,20 @@ public class IoTDBJDBCResultSet implements ResultSet {
 
   @Override
   public Timestamp getTimestamp(int columnIndex) throws SQLException {
-    return convertToTimestamp(getLong(columnIndex), statement.getTimeFactor());
+    try {
+      return ioTDBRpcDataSet.getTimestamp(columnIndex);
+    } catch (StatementExecutionException e) {
+      throw new SQLException(e.getMessage());
+    }
   }
 
   @Override
   public Timestamp getTimestamp(String columnName) throws SQLException {
-    return new Timestamp(getLong(columnName));
+    try {
+      return ioTDBRpcDataSet.getTimestamp(columnName);
+    } catch (StatementExecutionException | IllegalArgumentException e) {
+      throw new SQLException(e.getMessage());
+    }
   }
 
   @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 109b20b7ec8..42d716f497f 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
@@ -684,29 +684,11 @@ public class IoTDBRelationalDatabaseMetadata extends 
IoTDBAbstractDatabaseMetada
       throws SQLException {
 
     Statement stmt = connection.createStatement();
-    boolean legacyMode = true;
-    ResultSet rs;
-
+    List<ColumnInfo> columns;
     try {
-      String sql =
-          String.format(
-              "select * from information_schema.columns where database like 
'%s'  escape '\\' and table_name like '%s' escape '\\' and (category='TAG' or 
category='TIME')",
-              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", removeEscape(schemaPattern), 
removeEscape(tableNamePattern));
-        rs = stmt.executeQuery(sql);
-        legacyMode = false;
-      } catch (SQLException e1) {
-        LOGGER.error(SHOW_TABLES_ERROR_MSG, e.getMessage());
-        throw e;
-      }
-
+      // The arguments are names rather than patterns, but some tools escape 
them anyway
+      columns =
+          listColumns(stmt, toExactPattern(schemaPattern), 
toExactPattern(tableNamePattern), null);
     } finally {
       stmt.close();
     }
@@ -726,65 +708,41 @@ public class IoTDBRelationalDatabaseMetadata extends 
IoTDBAbstractDatabaseMetada
             TSDataType.TEXT,
             TSDataType.INT32,
             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);
-    }
 
-    int count = 1;
-    while (rs.next()) {
-      String columnName = legacyMode ? rs.getString("column_name") : 
rs.getString("ColumnName");
-      String category = legacyMode ? rs.getString("category") : 
rs.getString("Category");
+    Map<List<String>, Integer> keySeqs = new HashMap<>();
+    for (ColumnInfo column : columns) {
+      String category = column.category();
       if (category.equals("TAG") || category.equals("TIME")) {
         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(schemaPattern);
-          } else if (i == 2) {
-            valueInRow.add(tableNamePattern);
-          } else if (i == 3) {
-            valueInRow.add(columnName);
-          } else if (i == 4) {
-            valueInRow.add(count++);
-          } else {
-            valueInRow.add(PRIMARY);
-          }
-        }
+        // TABLE_CAT: the table model has no catalogs
+        valueInRow.add(null);
+        valueInRow.add(column.database());
+        valueInRow.add(column.tableName());
+        valueInRow.add(column.columnName());
+        valueInRow.add(
+            keySeqs.merge(Arrays.asList(column.database(), 
column.tableName()), 1, Integer::sum));
+        valueInRow.add(PRIMARY);
         valuesList.add(valueInRow);
       }
     }
 
-    ByteBuffer tsBlock = null;
-    try {
-      tsBlock = convertTsBlock(valuesList, tsDataTypeList);
-    } catch (IOException e) {
-      LOGGER.error(JdbcMessages.RELATIONAL_GET_PRIMARY_KEYS_ERROR, 
e.getMessage());
-    } finally {
-      close(null, stmt);
-    }
+    return toResultSet(stmt, fields, tsDataTypeList, valuesList);
+  }
 
-    return new IoTDBJDBCResultSet(
-        stmt,
-        columnNameList,
-        columnTypeList,
-        columnNameIndex,
-        true,
-        client,
-        null,
-        -1,
-        sessionId,
-        Collections.singletonList(tsBlock),
-        null,
-        (long) 60 * 1000,
-        false,
-        zoneId);
+  /** A pattern matching exactly the given name, which may already be escaped 
like a pattern. */
+  private static String toExactPattern(String name) {
+    if (name == null) {
+      return null;
+    }
+    StringBuilder pattern = new StringBuilder();
+    for (char c : removeEscape(name).toCharArray()) {
+      if (c == '%' || c == '_' || c == SEARCH_STRING_ESCAPE) {
+        pattern.append(SEARCH_STRING_ESCAPE);
+      }
+      pattern.append(c);
+    }
+    return pattern.toString();
   }
 
   @Override
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 21be0d33e9e..85f2489f324 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
@@ -297,6 +297,7 @@ public class IoTDBJDBCResultSetTest {
       Assert.assertEquals("2", resultSet.getString(1));
       Assert.assertEquals(new Timestamp(2), resultSet.getObject(1));
       Assert.assertEquals(new Timestamp(2), resultSet.getTimestamp(1));
+      Assert.assertEquals(new Timestamp(2), resultSet.getTimestamp("Time"));
 
       // The strictly-typed ones must report a SQLException, not an unchecked 
exception.
       assertTimeColumnRejected(() -> resultSet.getBoolean(1));
@@ -313,20 +314,7 @@ 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
-            })) {
+    try (ResultSet resultSet = executeTableModelQueryWithAllTypes(1_000_000, 
allTypesRow())) {
       Assert.assertTrue(resultSet.next());
       Assert.assertEquals(Boolean.TRUE, resultSet.getObject(1, Boolean.class));
       Assert.assertEquals(Integer.valueOf(42), resultSet.getObject(2, 
Integer.class));
@@ -362,28 +350,70 @@ public class IoTDBJDBCResultSetTest {
       Assert.assertNull(resultSet.getObject(8, Timestamp.class));
       Assert.assertNull(resultSet.getObject(9, LocalDate.class));
       Assert.assertTrue(resultSet.wasNull());
+      Assert.assertNull(resultSet.getObject("i", Integer.class));
     }
   }
 
   @SuppressWarnings("resource")
   @Test
   public void testGetObjectRejectsUnsupportedConversion() throws Exception {
+    try (ResultSet resultSet = executeTableModelQueryWithAllTypes(1_000, 
allTypesRow())) {
+      Assert.assertTrue(resultSet.next());
+      assertRejectedWithSqlException(() -> resultSet.getObject(6, 
Integer.class));
+      assertRejectedWithSqlException(() -> resultSet.getObject(1, Long.class));
+      assertRejectedWithSqlException(() -> resultSet.getObject(9, 
Timestamp.class));
+      assertRejectedWithSqlException(() -> resultSet.getObject(2, 
StringBuilder.class));
+      assertRejectedWithSqlException(() -> resultSet.getObject(2, (Class<?>) 
null));
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Test
+  public void testGetObjectByLabelConvertsToRequestedClass() throws Exception {
+    try (ResultSet resultSet = executeTableModelQueryWithAllTypes(1_000_000, 
allTypesRow())) {
+      Assert.assertTrue(resultSet.next());
+      Assert.assertEquals(Integer.valueOf(42), resultSet.getObject("i", 
Integer.class));
+      Assert.assertEquals(Long.valueOf(7L), resultSet.getObject("l", 
Long.class));
+      Assert.assertEquals("hello", resultSet.getObject("s", String.class));
+      Timestamp expectedTimestamp = new Timestamp(1_700_000_000_123L);
+      expectedTimestamp.setNanos(123_456_000);
+      Assert.assertEquals(expectedTimestamp, resultSet.getObject("ts", 
Timestamp.class));
+      Assert.assertEquals(LocalDate.of(2024, 1, 15), resultSet.getObject("dt", 
LocalDate.class));
+      assertRejectedWithSqlException(() -> resultSet.getObject("s", 
Integer.class));
+      assertRejectedWithSqlException(() -> 
resultSet.getObject("no_such_column", Integer.class));
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Test
+  public void testGetTimestampHonorsTimePrecisionByIndexAndLabel() throws 
Exception {
+    try (ResultSet resultSet = executeTableModelQueryWithAllTypes(1_000_000, 
allTypesRow())) {
+      Assert.assertTrue(resultSet.next());
+      Timestamp expectedTimestamp = new Timestamp(1_700_000_000_123L);
+      expectedTimestamp.setNanos(123_456_000);
+      Assert.assertEquals(expectedTimestamp, resultSet.getTimestamp(8));
+      Assert.assertEquals(expectedTimestamp, resultSet.getTimestamp("ts"));
+      assertRejectedWithSqlException(() -> 
resultSet.getTimestamp("no_such_column"));
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Test
+  public void testGetTimestampReturnsNullForSqlNull() throws Exception {
     try (ResultSet resultSet =
-        executeTableModelQueryWithAllTypes(
-            1_000, new Object[] {true, 42, 7L, 1.5F, 2.25D, "hello", new 
byte[0], 0L, 20240115})) {
+        executeTableModelQueryWithAllTypes(1_000, new 
Object[ALL_TYPES.size()])) {
       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));
+      Assert.assertNull(resultSet.getTimestamp(8));
+      Assert.assertTrue(resultSet.wasNull());
+      Assert.assertNull(resultSet.getTimestamp("ts"));
+      Assert.assertTrue(resultSet.wasNull());
     }
   }
 
-  private void assertGetObjectRejected(ResultSetRead read) {
+  private void assertRejectedWithSqlException(ResultSetRead read) {
     try {
       read.run();
-      Assert.fail("the conversion should have been rejected");
+      Assert.fail("the read should have been rejected");
     } catch (SQLException e) {
       // expected
     } catch (RuntimeException e) {
@@ -392,6 +422,13 @@ public class IoTDBJDBCResultSetTest {
     }
   }
 
+  /** One value of each type in ALL_TYPES; the TIMESTAMP is in microseconds. */
+  private static Object[] allTypesRow() {
+    return new Object[] {
+      true, 42, 7L, 1.5F, 2.25D, "hello", new byte[] {1, 2, 3}, 
1_700_000_000_123_456L, 20240115
+    };
+  }
+
   /** 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 {
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
index 8bbc3d015b9..2c4e311a59a 100644
--- 
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
@@ -436,6 +436,87 @@ public class IoTDBRelationalDatabaseMetadataTest {
     }
   }
 
+  // ========== getPrimaryKeys ==========
+
+  @Test
+  public void testGetPrimaryKeysReportsRealNamesOfTimeAndTagColumns() throws 
Exception {
+    responses.put(
+        COLUMNS_QUERY,
+        informationSchemaColumns(
+            informationSchemaColumn("db_1", "sensor_data", "ts", "TIMESTAMP", 
"TIME"),
+            informationSchemaColumn("db_1", "sensor_data", "region", "STRING", 
"TAG"),
+            informationSchemaColumn("db_1", "sensor_data", "model", "STRING", 
"ATTRIBUTE"),
+            informationSchemaColumn("db_1", "sensor_data", "plant", "STRING", 
"TAG"),
+            informationSchemaColumn("db_1", "sensor_data", "temperature", 
"FLOAT", "FIELD")));
+
+    List<String> keys = new ArrayList<>();
+    // Some tools escape the names as if they were patterns
+    try (ResultSet rs = metadata.getPrimaryKeys(null, "db\\_1", 
"sensor\\_data")) {
+      while (rs.next()) {
+        assertNull(rs.getString("TABLE_CAT"));
+        assertEquals("db_1", rs.getString("TABLE_SCHEM"));
+        assertEquals("sensor_data", rs.getString("TABLE_NAME"));
+        assertEquals("PRIMARY", rs.getString("PK_NAME"));
+        keys.add(rs.getString("COLUMN_NAME") + "#" + rs.getInt("KEY_SEQ"));
+      }
+    }
+    assertEquals(Arrays.asList("ts#1", "region#2", "plant#3"), keys);
+  }
+
+  @Test
+  public void testGetPrimaryKeysMatchesExactNamesWhetherEscapedOrNot() throws 
Exception {
+    responses.put(COLUMNS_QUERY, informationSchemaColumns());
+
+    metadata.getPrimaryKeys(null, "db_1", "it's").close();
+    metadata.getPrimaryKeys(null, "db\\_1", "sensor\\_data").close();
+    metadata.getPrimaryKeys(null, null, "t1").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"
+                + " where database like 'db\\_1' escape '\\'"
+                + " and table_name like 'sensor\\_data' escape '\\'",
+            "select * from information_schema.columns where table_name like 
't1' escape '\\'"),
+        executedStatements);
+  }
+
+  @Test
+  public void testGetPrimaryKeysReturnsEmptyResultSetForUnknownTable() throws 
Exception {
+    responses.put(COLUMNS_QUERY, informationSchemaColumns());
+
+    try (ResultSet rs = metadata.getPrimaryKeys(null, "db_1", 
"no_such_table")) {
+      assertFalse(rs.next());
+    }
+  }
+
+  @Test
+  public void testGetPrimaryKeysFallsBackToDescribeTable() 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[] {"region", "STRING", "TAG", "USING", null},
+            new Object[] {"temperature", "FLOAT", "FIELD", "USING", null}));
+
+    List<String> keys = new ArrayList<>();
+    try (ResultSet rs = metadata.getPrimaryKeys(null, "db_1", "sensor_data")) {
+      while (rs.next()) {
+        assertNull(rs.getString("TABLE_CAT"));
+        assertEquals("db_1", rs.getString("TABLE_SCHEM"));
+        assertEquals("sensor_data", rs.getString("TABLE_NAME"));
+        keys.add(rs.getString("COLUMN_NAME") + "#" + rs.getInt("KEY_SEQ"));
+      }
+    }
+    assertEquals(Arrays.asList("ts#1", "region#2"), keys);
+  }
+
   // ========== fake server responses ==========
 
   private static Object[] informationSchemaTable(String database, String 
table, String comment) {

Reply via email to