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) {
