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:04 UTC
(iotdb) 03/03: [JDBC] Accept read-only hint, default to auto-commit and fix client-side SQL binding
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);