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 <[email protected]> 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);
