You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ja...@apache.org on 2026/10/21 08:49:01 UTC
(iotdb) branch fix/jdbc-table-model-metadata-cancel created (now 5bd997c8f28)
This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a change to branch fix/jdbc-table-model-metadata-cancel
in repository https://gitbox.apache.org/repos/asf/iotdb.git
at 5bd997c8f28 [JDBC] Accept read-only hint, default to auto-commit and fix client-side SQL binding
This branch includes the following new commits:
new 4099bc4b3e8 [JDBC] Fix table-model metadata, prepared cancel/fetch size and getObject(Class)
new c0dcfbe0d49 [JDBC] Fix getPrimaryKeys names, NULL getTimestamp and getObject by label
new 5bd997c8f28 [JDBC] Accept read-only hint, default to auto-commit and fix client-side SQL binding
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(iotdb) 02/03: [JDBC] Fix getPrimaryKeys names, NULL getTimestamp and getObject by label
Posted by ja...@apache.org.
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 <ja...@gmail.com>
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) {
(iotdb) 01/03: [JDBC] Fix table-model metadata, prepared cancel/fetch size and getObject(Class)
Posted by ja...@apache.org.
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 <ja...@gmail.com>
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;
+ }
}
(iotdb) 03/03: [JDBC] Accept read-only hint, default to auto-commit and fix client-side SQL binding
Posted by ja...@apache.org.
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 5bd997c8f2845d4c4d283c5ca375d73e1cb47a82
Author: JackieTien97 <ja...@gmail.com>
AuthorDate: Wed Oct 21 16:07:15 2026 +0800
[JDBC] Accept read-only hint, default to auto-commit and fix client-side SQL binding
IoTDBConnection:
- setReadOnly records the hint instead of throwing, and isReadOnly
reports it. Trino calls setReadOnly(true) on every read connection.
- New connections are in auto-commit mode, as JDBC requires; IoTDB has
no transactions, so commit / rollback stay no-ops.
- getCatalog returns null in the table dialect, matching the NULL
TABLE_CAT of its DatabaseMetaData, and setCatalog(null) is ignored
instead of failing with an NPE. The tree dialect keeps "Apache IoTDB",
which its DatabaseMetaData compares against, now null-safely.
IoTDBTablePreparedStatement:
- setObject (all overloads) accepts java.time.LocalDate and binds it like
setDate.
- SQL completed on the client is split at '?' the way the table-model
lexer tokenizes: string literals, double-quoted and back-quoted
identifiers and comments hide '?', quotes are escaped only by doubling,
and a backslash is an ordinary character.
- NaN is written as the string 'NaN', which the server stores as NaN in
FLOAT / DOUBLE columns. Infinity has no accepted SQL form (insert and
CAST both reject it), so binding it there fails with a clear
SQLException; server-prepared queries still send it in binary.
Metadata:
- Report OBJECT columns with type name OBJECT and java.sql.Types.OTHER in
ResultSetMetaData (class String, as getObject describes the value) and
in DatabaseMetaData.getColumns.
---
.../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 2 +-
.../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 2 +-
.../iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java | 5 +
.../org/apache/iotdb/jdbc/IoTDBConnection.java | 17 +--
.../apache/iotdb/jdbc/IoTDBDatabaseMetadata.java | 2 +-
.../org/apache/iotdb/jdbc/IoTDBResultMetadata.java | 11 +-
.../iotdb/jdbc/IoTDBTablePreparedStatement.java | 92 ++++++++++-----
.../org/apache/iotdb/jdbc/IoTDBConnectionTest.java | 39 +++++++
.../iotdb/jdbc/IoTDBDatabaseMetadataTest.java | 20 ++++
.../jdbc/IoTDBRelationalDatabaseMetadataTest.java | 14 +++
.../apache/iotdb/jdbc/IoTDBResultMetadataTest.java | 14 +++
.../jdbc/IoTDBTablePreparedStatementTest.java | 130 +++++++++++++++++++++
12 files changed, 310 insertions(+), 38 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 fe2d071b066..ea3eea76fb8 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
@@ -164,7 +164,6 @@ public final class JdbcMessages {
"Does not support setTransactionIsolation";
public static final String NOT_SUPPORT_GET_TYPE_MAP = "Does not support getTypeMap";
public static final String NOT_SUPPORT_SET_TYPE_MAP = "Does not support setTypeMap";
- public static final String NOT_SUPPORT_READ_ONLY = "Does not support readOnly";
public static final String NOT_SUPPORT_NATIVE_SQL = "Does not support nativeSQL";
public static final String NOT_SUPPORT_RELEASE_SAVEPOINT =
"Does not support releaseSavepoint";
@@ -230,5 +229,6 @@ public final class JdbcMessages {
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";
+ public static final String EXCEPTION_CANNOT_BIND_ARG_IOTDB_SQL_HAS_NO_LITERAL_INFINITE_FLOATING_4068FF32 = "Cannot bind %s: IoTDB SQL has no literal for an infinite floating-point value";
}
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 ccbfc04a02f..727a8d7809c 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
@@ -163,7 +163,6 @@ public final class JdbcMessages {
"不支持 setTransactionIsolation";
public static final String NOT_SUPPORT_GET_TYPE_MAP = "不支持 getTypeMap";
public static final String NOT_SUPPORT_SET_TYPE_MAP = "不支持 setTypeMap";
- public static final String NOT_SUPPORT_READ_ONLY = "不支持 readOnly";
public static final String NOT_SUPPORT_NATIVE_SQL = "不支持 nativeSQL";
public static final String NOT_SUPPORT_RELEASE_SAVEPOINT =
"不支持 releaseSavepoint";
@@ -229,5 +228,6 @@ public final class JdbcMessages {
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";
+ public static final String EXCEPTION_CANNOT_BIND_ARG_IOTDB_SQL_HAS_NO_LITERAL_INFINITE_FLOATING_4068FF32 = "无法绑定 %s:IoTDB SQL 没有表示无穷大浮点数的字面量";
}
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 1b0cc9166b4..b52f3bba55e 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
@@ -92,6 +92,7 @@ public abstract class IoTDBAbstractDatabaseMetadata implements DatabaseMetaData
protected static final String BLOB = "BLOB";
protected static final String TIMESTAMP = "TIMESTAMP";
protected static final String DATE = "DATE";
+ protected static final String OBJECT = "OBJECT";
protected static String sqlKeywordsThatArentSQL92;
@@ -479,6 +480,9 @@ public abstract class IoTDBAbstractDatabaseMetadata implements DatabaseMetaData
return Types.TIMESTAMP;
case DATE:
return Types.DATE;
+ case OBJECT:
+ // Database-specific: getObject() describes the value instead of returning it
+ return Types.OTHER;
default:
break;
}
@@ -500,6 +504,7 @@ public abstract class IoTDBAbstractDatabaseMetadata implements DatabaseMetaData
case "TEXT":
case BLOB:
case STRING:
+ case OBJECT:
return Integer.MAX_VALUE;
case TIMESTAMP:
return 3;
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
index cbd501aa887..c207a123964 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBConnection.java
@@ -98,7 +98,10 @@ public class IoTDBConnection implements Connection {
private ZoneId zoneId;
private Charset charset;
- private boolean autoCommit;
+ // IoTDB has no transactions, so every statement takes effect at once as JDBC auto-commit does
+ private boolean autoCommit = true;
+ // Recorded as the hint JDBC defines it to be; IoTDB has no read-only connection mode
+ private boolean readOnly;
private String url;
public String getUserName() {
@@ -143,7 +146,6 @@ public class IoTDBConnection implements Connection {
openSession();
// Wrap the client with a thread-safe proxy to serialize the RPC calls
setClient(RpcUtils.newSynchronizedClient(getClient()));
- autoCommit = false;
}
public String getUrl() {
@@ -270,13 +272,14 @@ public class IoTDBConnection implements Connection {
@Override
public String getCatalog() {
- return APACHE_IOTDB;
+ // The table model has no catalogs, matching the NULL TABLE_CAT of its DatabaseMetaData
+ return getSqlDialect().equals(Constant.TABLE_DIALECT) ? null : APACHE_IOTDB;
}
@Override
public void setCatalog(String arg0) throws SQLException {
if (getSqlDialect().equals(Constant.TABLE_DIALECT)) {
- if (APACHE_IOTDB.equals(arg0)) {
+ if (arg0 == null || APACHE_IOTDB.equals(arg0)) {
return;
}
for (String str : IoTDBRelationalDatabaseMetadata.allIotdbTableSQLKeywords) {
@@ -400,14 +403,12 @@ public class IoTDBConnection implements Connection {
@Override
public boolean isReadOnly() {
- return false;
+ return readOnly;
}
@Override
public void setReadOnly(boolean readonly) throws SQLException {
- if (readonly) {
- throw new SQLException(JdbcMessages.NOT_SUPPORT_READ_ONLY);
- }
+ this.readOnly = readonly;
}
@Override
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadata.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadata.java
index 0e5d5b64a92..a9c03650e50 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadata.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadata.java
@@ -387,7 +387,7 @@ public class IoTDBDatabaseMetadata extends IoTDBAbstractDatabaseMetadata {
throws SQLException {
try (Statement stmt = this.connection.createStatement()) {
- if (this.connection.getCatalog().equals(catalog)) {
+ if (catalog != null && catalog.equals(this.connection.getCatalog())) {
catalog = null;
}
diff --git a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBResultMetadata.java b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBResultMetadata.java
index 59279d88465..91a9c8cfbb2 100644
--- a/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBResultMetadata.java
+++ b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBResultMetadata.java
@@ -51,6 +51,7 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
private static final String DATE = "DATE";
private static final String TIMESTAMP = "TIMESTAMP";
+ private static final String OBJECT = "OBJECT";
/** Constructor of IoTDBResultMetadata. */
public IoTDBResultMetadata(
@@ -165,6 +166,9 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
return Date.class.getName();
case BLOB:
return "byte[]";
+ case OBJECT:
+ // getObject() describes an OBJECT value with a String such as "(Object) 1.2 MB"
+ return String.class.getName();
default:
break;
}
@@ -232,6 +236,8 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
return Types.TIMESTAMP;
case BLOB:
return Types.BLOB;
+ case OBJECT:
+ return Types.OTHER;
default:
break;
}
@@ -255,7 +261,8 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
|| STRING.equals(typeString)
|| BLOB.equals(typeString)
|| DATE.equals(typeString)
- || TIMESTAMP.equals(typeString)) {
+ || TIMESTAMP.equals(typeString)
+ || OBJECT.equals(typeString)) {
return typeString;
}
return null;
@@ -282,6 +289,7 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
case TEXT:
case BLOB:
case STRING:
+ case OBJECT:
return Integer.MAX_VALUE;
case TIMESTAMP:
return 3;
@@ -309,6 +317,7 @@ public class IoTDBResultMetadata implements ResultSetMetaData {
case STRING:
case TIMESTAMP:
case DATE:
+ case OBJECT:
return 0;
case FLOAT:
return 6;
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 e0822a32ea9..8efb287389d 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
@@ -60,6 +60,7 @@ import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
+import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
@@ -373,14 +374,32 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa
public void setFloat(int parameterIndex, float x) throws SQLException {
checkParameterIndex(parameterIndex);
setPreparedParameterValue(parameterIndex, x, Types.FLOAT);
- this.parameters.put(parameterIndex, Float.toString(x));
+ this.parameters.put(parameterIndex, floatingPointLiteral(x, Float.toString(x)));
}
@Override
public void setDouble(int parameterIndex, double x) throws SQLException {
checkParameterIndex(parameterIndex);
setPreparedParameterValue(parameterIndex, x, Types.DOUBLE);
- this.parameters.put(parameterIndex, Double.toString(x));
+ this.parameters.put(parameterIndex, floatingPointLiteral(x, Double.toString(x)));
+ }
+
+ /**
+ * The literal for a FLOAT or DOUBLE parameter of SQL completed on the client. IoTDB SQL has no
+ * NaN or Infinity literal, but stores the string 'NaN' as NaN and rejects Infinity in any form.
+ */
+ private String floatingPointLiteral(double value, String text) throws SQLException {
+ if (serverSidePrepared || Double.isFinite(value)) {
+ return text;
+ }
+ if (Double.isNaN(value)) {
+ return "'NaN'";
+ }
+ throw new SQLException(
+ String.format(
+ JdbcMessages
+ .EXCEPTION_CANNOT_BIND_ARG_IOTDB_SQL_HAS_NO_LITERAL_INFINITE_FLOATING_4068FF32,
+ text));
}
@Override
@@ -404,9 +423,13 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa
@Override
public void setDate(int parameterIndex, Date x) throws SQLException {
+ setLocalDate(parameterIndex, x.toLocalDate());
+ }
+
+ private void setLocalDate(int parameterIndex, LocalDate x) throws SQLException {
checkParameterIndex(parameterIndex);
// Use ISO-8601 format YYYY-MM-DD for DATE columns
- String dateStr = x.toLocalDate().toString();
+ String dateStr = x.toString();
setPreparedParameterValue(parameterIndex, dateStr, Types.DATE);
this.parameters.put(parameterIndex, "'" + dateStr + "'");
}
@@ -497,6 +520,8 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa
setTimestamp(parameterIndex, (Timestamp) x);
} else if (x instanceof Date) {
setDate(parameterIndex, (Date) x);
+ } else if (x instanceof LocalDate) {
+ setLocalDate(parameterIndex, (LocalDate) x);
} else if (x instanceof Time) {
setTime(parameterIndex, (Time) x);
} else if (x instanceof byte[]) {
@@ -723,36 +748,51 @@ public class IoTDBTablePreparedStatement extends IoTDBStatement implements Prepa
return newSql.toString();
}
+ /**
+ * Splits the SQL at its parameter markers, skipping string literals, quoted identifiers and
+ * comments as the table-model lexer does. Quotes are escaped only by doubling them; a backslash
+ * is an ordinary character.
+ */
private List<String> splitSqlStatement(final String sql) {
List<String> parts = new ArrayList<>();
- int apCount = 0;
int off = 0;
- boolean skip = false;
-
- for (int i = 0; i < sql.length(); i++) {
+ int i = 0;
+ while (i < sql.length()) {
char c = sql.charAt(i);
- if (skip) {
- skip = false;
- continue;
- }
- switch (c) {
- case '\'':
- apCount++;
- break;
- case '\\':
- skip = true;
- break;
- case '?':
- if ((apCount & 1) == 0) {
- parts.add(sql.substring(off, i));
- off = i + 1;
- }
- break;
- default:
- break;
+ if (c == '\'' || c == '"' || c == '`') {
+ i = skipQuoted(sql, i, c);
+ } else if (sql.startsWith("--", i)) {
+ int end = sql.indexOf('\n', i);
+ i = end < 0 ? sql.length() : end + 1;
+ } else if (sql.startsWith("/*", i)) {
+ int end = sql.indexOf("*/", i + 2);
+ i = end < 0 ? sql.length() : end + 2;
+ } else {
+ if (c == '?') {
+ parts.add(sql.substring(off, i));
+ off = i + 1;
+ }
+ i++;
}
}
parts.add(sql.substring(off));
return parts;
}
+
+ /** Returns the index just past the quoted text that starts at {@code start}. */
+ private static int skipQuoted(String sql, int start, char quote) {
+ int i = start + 1;
+ while (i < sql.length()) {
+ if (sql.charAt(i) == quote) {
+ if (i + 1 < sql.length() && sql.charAt(i + 1) == quote) {
+ // A doubled quote stands for the quote character itself
+ i += 2;
+ continue;
+ }
+ return i + 1;
+ }
+ i++;
+ }
+ return sql.length();
+ }
}
diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBConnectionTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBConnectionTest.java
index 2919e51d265..be3c7de929e 100644
--- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBConnectionTest.java
+++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBConnectionTest.java
@@ -43,6 +43,11 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IoTDBConnectionTest {
@@ -119,4 +124,38 @@ public class IoTDBConnectionTest {
connection.setQueryTimeout(60);
Assert.assertEquals(60, connection.getQueryTimeout());
}
+
+ @Test
+ public void testReadOnlyIsAcceptedAsHint() throws SQLException {
+ Assert.assertFalse(connection.isReadOnly());
+ connection.setReadOnly(true);
+ Assert.assertTrue(connection.isReadOnly());
+ connection.setReadOnly(false);
+ Assert.assertFalse(connection.isReadOnly());
+ }
+
+ @Test
+ public void testNewConnectionIsInAutoCommitMode() {
+ Assert.assertTrue(new IoTDBConnection().getAutoCommit());
+ }
+
+ @Test
+ public void testCatalogIsNullInTableDialect() throws SQLException {
+ IoTDBConnection tableConnection = spy(new IoTDBConnection());
+ doReturn(Constant.TABLE_DIALECT).when(tableConnection).getSqlDialect();
+ Assert.assertNull(tableConnection.getCatalog());
+
+ // The tree model keeps its catalog, which its DatabaseMetaData compares against
+ Assert.assertEquals("Apache IoTDB", connection.getCatalog());
+ }
+
+ @Test
+ public void testSetCatalogToNullIsIgnoredInTableDialect() throws SQLException {
+ IoTDBConnection tableConnection = spy(new IoTDBConnection());
+ doReturn(Constant.TABLE_DIALECT).when(tableConnection).getSqlDialect();
+
+ tableConnection.setCatalog(null);
+
+ verify(tableConnection, never()).prepareStatement(anyString());
+ }
}
diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadataTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadataTest.java
index ab29a141399..a4f31979686 100644
--- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadataTest.java
+++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBDatabaseMetadataTest.java
@@ -159,6 +159,26 @@ public class IoTDBDatabaseMetadataTest {
Assert.assertEquals("PKTABLE_SCHEM", resultSet.getMetaData().getColumnName(3));
}
+ @Test
+ public void testGetColumnsToleratesConnectionWithoutCatalog() throws Exception {
+ // A connection that switched to the table dialect reports no catalog
+ when(connection.getCatalog()).thenReturn(null);
+ TSExecuteStatementResp showTimeseriesResp =
+ new TSExecuteStatementResp(RpcUtils.getStatus(TSStatusCode.SUCCESS_STATUS));
+ showTimeseriesResp.setQueryId(1L);
+ showTimeseriesResp.setColumns(Collections.singletonList("Timeseries"));
+ showTimeseriesResp.setDataTypeList(Collections.singletonList("TEXT"));
+ showTimeseriesResp.setIgnoreTimeStamp(true);
+ showTimeseriesResp.setColumnIndex2TsBlockColumnIndexList(
+ new ArrayList<>(Collections.singletonList(0)));
+ showTimeseriesResp.setQueryResult(Collections.emptyList());
+ when(client.executeQueryStatementV2(any(TSExecuteStatementReq.class)))
+ .thenReturn(showTimeseriesResp);
+ when(client.closeOperation(any())).thenReturn(RpcUtils.getStatus(TSStatusCode.SUCCESS_STATUS));
+
+ Assert.assertNotNull(databaseMetaData.getColumns(null, "root.sg", null, null));
+ }
+
@Test
public void testGetIndexInfo() throws SQLException {
ResultSet resultSet = databaseMetaData.getIndexInfo(null, null, null, false, false);
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 2c4e311a59a..6517e7dbf2b 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
@@ -319,6 +319,20 @@ public class IoTDBRelationalDatabaseMetadataTest {
}
}
+ @Test
+ public void testGetColumnsReportsObjectAsDatabaseSpecificType() throws Exception {
+ responses.put(
+ COLUMNS_QUERY,
+ informationSchemaColumns(
+ informationSchemaColumn("db_1", "files", "content", "OBJECT", "FIELD")));
+
+ try (ResultSet rs = metadata.getColumns(null, "db_1", "files", null)) {
+ assertTrue(rs.next());
+ assertEquals("OBJECT", rs.getString("TYPE_NAME"));
+ assertEquals(Types.OTHER, rs.getInt("DATA_TYPE"));
+ }
+ }
+
@Test
public void testGetColumnsDerivesNullabilityFromTimeCategory() throws Exception {
responses.put(
diff --git a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBResultMetadataTest.java b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBResultMetadataTest.java
index dc9869bddda..13d84c6d76a 100644
--- a/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBResultMetadataTest.java
+++ b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBResultMetadataTest.java
@@ -173,4 +173,18 @@ public class IoTDBResultMetadataTest {
assertEquals(metadata.getColumnType(i + 1), types[i - 1]);
}
}
+
+ @Test
+ public void testObjectColumnIsDescribed() throws SQLException {
+ metadata =
+ new IoTDBResultMetadata(
+ false, null, "QUERY", Arrays.asList("file"), Arrays.asList("OBJECT"), true);
+
+ assertEquals("OBJECT", metadata.getColumnTypeName(1));
+ assertEquals(Types.OTHER, metadata.getColumnType(1));
+ // getObject() renders an OBJECT value as a String such as "(Object) 1.2 MB"
+ assertEquals(String.class.getName(), metadata.getColumnClassName(1));
+ assertEquals(Integer.MAX_VALUE, metadata.getPrecision(1));
+ assertEquals(0, metadata.getScale(1));
+ }
}
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 83e91ddddeb..fa00f2add37 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
@@ -42,6 +42,8 @@ import org.mockito.stubbing.Answer;
import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.sql.Timestamp;
+import java.sql.Types;
+import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
@@ -363,6 +365,134 @@ public class IoTDBTablePreparedStatementTest {
"INSERT INTO t(time, s1) VALUES(1700000000123456, 1)", argument.getValue().getStatement());
}
+ // ========== LocalDate, client-side SQL completion and NaN/Infinity ==========
+
+ @Test
+ public void testSetObjectAcceptsLocalDateInClientCompletedSql() throws Exception {
+ LocalDate date = LocalDate.of(2024, 1, 15);
+ String expected = "INSERT INTO t(time, d) VALUES(1, '2024-01-15')";
+ String sql = "INSERT INTO t(time, d) VALUES(1, ?)";
+ assertEquals(expected, clientCompletedSql(sql, ps -> ps.setObject(1, date)));
+ assertEquals(expected, clientCompletedSql(sql, ps -> ps.setObject(1, date, Types.DATE)));
+ assertEquals(expected, clientCompletedSql(sql, ps -> ps.setObject(1, date, Types.DATE, 0)));
+ }
+
+ @SuppressWarnings("resource")
+ @Test
+ public void testSetObjectBindsLocalDateLikeSetDate() throws Exception {
+ IoTDBTablePreparedStatement ps =
+ new IoTDBTablePreparedStatement(
+ connection, client, sessionId, "SELECT * FROM t WHERE d = ? AND e = ?", zoneId);
+ ps.setObject(1, LocalDate.of(2024, 1, 15));
+ ps.setDate(2, java.sql.Date.valueOf("2024-01-15"));
+ ps.execute();
+
+ List<PreparedParameterSerde.DeserializedParam> parameters = lastPreparedParameters();
+ assertEquals("2024-01-15", parameters.get(0).value);
+ assertEquals(parameters.get(1).type, parameters.get(0).type);
+ assertEquals(parameters.get(1).value, parameters.get(0).value);
+ }
+
+ @Test
+ public void testQuotedIdentifiersHideQuestionMarksAndQuotes() throws Exception {
+ assertEquals(
+ "INSERT INTO \"my?table\"(time, s1) VALUES(1, 'a')",
+ clientCompletedSql("INSERT INTO \"my?table\"(time, s1) VALUES(?, ?)", this::bindRow));
+ assertEquals(
+ "INSERT INTO \"it's\"(time, s1) VALUES(1, 'a')",
+ clientCompletedSql("INSERT INTO \"it's\"(time, s1) VALUES(?, ?)", this::bindRow));
+ assertEquals(
+ "INSERT INTO `a?b`(time, s1) VALUES(1, 'a')",
+ clientCompletedSql("INSERT INTO `a?b`(time, s1) VALUES(?, ?)", this::bindRow));
+ }
+
+ @Test
+ public void testStringLiteralsEscapeQuotesOnlyByDoublingThem() throws Exception {
+ // A backslash is an ordinary character, so 'C:\' ends at its second quote
+ assertEquals(
+ "INSERT INTO t(time, s1, s2) VALUES(1, 'C:\\', 'a')",
+ clientCompletedSql("INSERT INTO t(time, s1, s2) VALUES(?, 'C:\\', ?)", this::bindRow));
+ assertEquals(
+ "INSERT INTO t(time, s1, s2) VALUES(1, 'it''s ?', 'a')",
+ clientCompletedSql("INSERT INTO t(time, s1, s2) VALUES(?, 'it''s ?', ?)", this::bindRow));
+ }
+
+ @Test
+ public void testCommentsHideQuestionMarks() throws Exception {
+ assertEquals(
+ "INSERT INTO t(time, s1) VALUES(1, 'a') -- why?\n",
+ clientCompletedSql("INSERT INTO t(time, s1) VALUES(?, ?) -- why?\n", this::bindRow));
+ assertEquals(
+ "INSERT /* ? */ INTO t(time, s1) VALUES(1, 'a')",
+ clientCompletedSql("INSERT /* ? */ INTO t(time, s1) VALUES(?, ?)", this::bindRow));
+ }
+
+ @Test
+ public void testNaNIsSentAsStringInClientCompletedSql() throws Exception {
+ // The server has no NaN literal but stores the string 'NaN' as NaN in FLOAT/DOUBLE columns
+ assertEquals(
+ "INSERT INTO t(time, d, f) VALUES(1, 'NaN', 'NaN')",
+ clientCompletedSql(
+ "INSERT INTO t(time, d, f) VALUES(1, ?, ?)",
+ ps -> {
+ ps.setDouble(1, Double.NaN);
+ ps.setFloat(2, Float.NaN);
+ }));
+ }
+
+ @SuppressWarnings("resource")
+ @Test
+ public void testInfinityIsRejectedInClientCompletedSql() throws Exception {
+ IoTDBTablePreparedStatement ps =
+ new IoTDBTablePreparedStatement(
+ connection, client, sessionId, "INSERT INTO t(time, d, f) VALUES(1, ?, ?)", zoneId);
+ assertThrows(SQLException.class, () -> ps.setDouble(1, Double.POSITIVE_INFINITY));
+ assertThrows(SQLException.class, () -> ps.setFloat(2, Float.NEGATIVE_INFINITY));
+ }
+
+ @SuppressWarnings("resource")
+ @Test
+ public void testInfinityIsBoundAsBinaryInServerPreparedQuery() throws Exception {
+ IoTDBTablePreparedStatement ps =
+ new IoTDBTablePreparedStatement(
+ connection, client, sessionId, "SELECT * FROM t WHERE d < ?", zoneId);
+ ps.setDouble(1, Double.POSITIVE_INFINITY);
+ ps.execute();
+
+ assertEquals(Double.POSITIVE_INFINITY, lastPreparedParameters().get(0).value);
+ }
+
+ @FunctionalInterface
+ private interface ParameterBinder {
+ void bind(IoTDBTablePreparedStatement ps) throws SQLException;
+ }
+
+ private void bindRow(IoTDBTablePreparedStatement ps) throws SQLException {
+ ps.setLong(1, 1);
+ ps.setString(2, "a");
+ }
+
+ /** Runs a statement that the driver completes on the client and returns the SQL it sends. */
+ @SuppressWarnings("resource")
+ private String clientCompletedSql(String sql, ParameterBinder binder) throws Exception {
+ when(client.executeUpdateStatement(any(TSExecuteStatementReq.class)))
+ .thenReturn(new TSExecuteStatementResp(RpcUtils.SUCCESS_STATUS));
+ IoTDBTablePreparedStatement ps =
+ new IoTDBTablePreparedStatement(connection, client, sessionId, sql, zoneId);
+ binder.bind(ps);
+ ps.executeUpdate();
+
+ ArgumentCaptor<TSExecuteStatementReq> argument =
+ ArgumentCaptor.forClass(TSExecuteStatementReq.class);
+ verify(client, atLeastOnce()).executeUpdateStatement(argument.capture());
+ return argument.getValue().getStatement();
+ }
+
+ private List<PreparedParameterSerde.DeserializedParam> lastPreparedParameters() throws Exception {
+ return PreparedParameterSerde.deserialize(
+ ByteBuffer.wrap(lastPreparedExecution().getParameters()));
+ }
+
private static TSExecuteStatementResp preparedQueryResp(long queryId) {
TSExecuteStatementResp resp = new TSExecuteStatementResp(RpcUtils.SUCCESS_STATUS);
resp.setQueryId(queryId);