This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 7aa8e63a7c6 [JDBC] Fix table-model driver defects that break generic
JDBC clients (#18962)
7aa8e63a7c6 is described below
commit 7aa8e63a7c6be84a5189006a2e5b884ab33f2a90
Author: Jackie Tien <[email protected]>
AuthorDate: Fri Oct 23 17:00:34 2026 +0800
[JDBC] Fix table-model driver defects that break generic JDBC clients
(#18962)
---
.../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 7 +-
.../org/apache/iotdb/jdbc/i18n/JdbcMessages.java | 7 +-
.../iotdb/jdbc/IoTDBAbstractDatabaseMetadata.java | 12 +-
.../org/apache/iotdb/jdbc/IoTDBConnection.java | 17 +-
.../apache/iotdb/jdbc/IoTDBDatabaseMetadata.java | 2 +-
.../org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java | 79 ++-
.../org/apache/iotdb/jdbc/IoTDBResultMetadata.java | 11 +-
.../java/org/apache/iotdb/jdbc/IoTDBStatement.java | 27 +-
.../iotdb/jdbc/IoTDBTablePreparedStatement.java | 128 ++++-
.../IoTDBRelationalDatabaseMetadata.java | 555 +++++++++++-------
.../org/apache/iotdb/jdbc/IoTDBConnectionTest.java | 39 ++
.../iotdb/jdbc/IoTDBDatabaseMetadataTest.java | 20 +
.../apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java | 209 +++++++
.../jdbc/IoTDBRelationalDatabaseMetadataTest.java | 634 +++++++++++++++++++++
.../apache/iotdb/jdbc/IoTDBResultMetadataTest.java | 14 +
.../jdbc/IoTDBTablePreparedStatementTest.java | 313 ++++++++++
.../main/java/org/apache/iotdb/rpc/RpcUtils.java | 5 +-
.../java/org/apache/iotdb/rpc/RpcUtilsTest.java | 28 +
18 files changed, 1826 insertions(+), 281 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..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";
@@ -225,5 +224,11 @@ 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: {}";
+ 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 a1cc83ea5e6..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";
@@ -224,5 +223,11 @@ 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 = "查询表信息出错:{}";
+ 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 0c50512e80b..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;
@@ -415,10 +416,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();
}
@@ -474,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;
}
@@ -495,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/IoTDBJDBCResultSet.java
b/iotdb-client/jdbc/src/main/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSet.java
index 3ea3c52320d..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
@@ -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;
@@ -67,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";
@@ -645,13 +644,69 @@ 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
- 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
@@ -741,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/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/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..3c95afa107d 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;
@@ -199,12 +200,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) {
@@ -370,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
@@ -401,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 + "'");
}
@@ -446,10 +472,32 @@ 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);
+ long fraction = x.getNanos() / (1_000_000_000 / timeFactor);
+ try {
+ if (seconds < 0 && fraction > 0) {
+ // Borrow a second, as Instant.toEpochMilli() does, so that the
product cannot underflow
+ // for a value whose positive fraction brings it back into range
+ return Math.addExact(Math.multiplyExact(seconds + 1, timeFactor),
fraction - timeFactor);
+ }
+ return Math.addExact(Math.multiplyExact(seconds, timeFactor), fraction);
+ } catch (ArithmeticException e) {
+ throw new SQLException(
+ String.format(
+
JdbcMessages.EXCEPTION_TIMESTAMP_ARG_OUT_RANGE_TIME_PRECISION_ARG_E25E3001,
+ x,
+ RpcUtils.getTimePrecision(timeFactor)),
+ e);
+ }
}
@Override
@@ -477,6 +525,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[]) {
@@ -703,36 +753,54 @@ 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)) {
+ // Like the lexer's SIMPLE_COMMENT, the comment ends at the first CR
or LF
+ i += 2;
+ while (i < sql.length() && sql.charAt(i) != '\r' && sql.charAt(i) !=
'\n') {
+ i++;
+ }
+ } 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/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..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
@@ -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,41 +597,98 @@ 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 {
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();
}
@@ -578,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
@@ -653,4 +759,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/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/IoTDBJDBCResultSetTest.java
b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBJDBCResultSetTest.java
index 5805fb10330..0446e6473f3 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;
@@ -279,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));
@@ -292,6 +311,196 @@ public class IoTDBJDBCResultSetTest {
}
}
+ @SuppressWarnings("resource")
+ @Test
+ public void testGetObjectConvertsToRequestedClass() throws Exception {
+ 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));
+ 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());
+ 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 testGetTimestampKeepsPreEpochValues() throws Exception {
+ Object[] row = allTypesRow();
+ // 1.5 seconds before the epoch, in milliseconds
+ row[7] = -1_500L;
+ try (ResultSet resultSet = executeTableModelQueryWithAllTypes(1_000, row))
{
+ Assert.assertTrue(resultSet.next());
+ Assert.assertEquals(new Timestamp(-1_500L),
resultSet.getTimestamp("ts"));
+ Assert.assertEquals(new Timestamp(-1_500L), resultSet.getTimestamp(8));
+ Assert.assertEquals(new Timestamp(-1_500L), resultSet.getObject(8,
Timestamp.class));
+ }
+ }
+
+ @SuppressWarnings("resource")
+ @Test
+ public void testGetTimestampReturnsNullForSqlNull() throws Exception {
+ try (ResultSet resultSet =
+ executeTableModelQueryWithAllTypes(1_000, new
Object[ALL_TYPES.size()])) {
+ Assert.assertTrue(resultSet.next());
+ Assert.assertNull(resultSet.getTimestamp(8));
+ Assert.assertTrue(resultSet.wasNull());
+ Assert.assertNull(resultSet.getTimestamp("ts"));
+ Assert.assertTrue(resultSet.wasNull());
+ }
+ }
+
+ private void assertRejectedWithSqlException(ResultSetRead read) {
+ try {
+ read.run();
+ Assert.fail("the read should have been rejected");
+ } catch (SQLException e) {
+ // expected
+ } catch (RuntimeException e) {
+ Assert.fail(
+ "expected a SQLException but got " + e.getClass().getName() + ": " +
e.getMessage());
+ }
+ }
+
+ /** 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 {
+ 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..6517e7dbf2b
--- /dev/null
+++
b/iotdb-client/jdbc/src/test/java/org/apache/iotdb/jdbc/IoTDBRelationalDatabaseMetadataTest.java
@@ -0,0 +1,634 @@
+/*
+ * 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 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(
+ 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());
+ }
+ }
+
+ // ========== 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) {
+ 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/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 dd1c7ecbcb2..d60cd8afdcf 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,23 @@ 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.sql.Types;
+import java.time.Instant;
+import java.time.LocalDate;
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 +254,300 @@ 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));
+
+ // ...and down to 1677-09-21T00:12:43.145224192Z, which is Long.MIN_VALUE
nanoseconds.
+ Timestamp beforeLowerBound =
Timestamp.from(Instant.parse("1677-09-21T00:12:43.145224191Z"));
+ assertThrows(SQLException.class, () -> ps.setTimestamp(1,
beforeLowerBound));
+ }
+
+ @Test
+ public void testSetTimestampAcceptsNanosecondLowerBound() throws Exception {
+ assertEquals(
+ Long.MIN_VALUE + 1,
+ preparedTimestampValue(
+ 1_000_000_000,
Timestamp.from(Instant.parse("1677-09-21T00:12:43.145224193Z"))));
+ assertEquals(
+ Long.MIN_VALUE,
+ preparedTimestampValue(
+ 1_000_000_000,
Timestamp.from(Instant.parse("1677-09-21T00:12:43.145224192Z"))));
+ }
+
+ @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());
+ }
+
+ // ========== 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 testLineCommentEndsAtCarriageReturnOrLineFeed() throws Exception
{
+ // As in the lexer's SIMPLE_COMMENT rule, a lone CR ends the comment just
like LF or CRLF
+ assertEquals(
+ "INSERT -- comment\rINTO t(time, s1) VALUES(1, 'a')\n",
+ clientCompletedSql("INSERT -- comment\rINTO t(time, s1) VALUES(?,
?)\n", this::bindRow));
+ assertEquals(
+ "INSERT -- comment\r\nINTO t(time, s1) VALUES(1, 'a')",
+ clientCompletedSql("INSERT -- comment\r\nINTO 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);
+ 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;
+ }
}
diff --git
a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcUtils.java
b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcUtils.java
index 9c88bf3f8b7..4c4c3c20989 100644
--- a/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcUtils.java
+++ b/iotdb-client/service-rpc/src/main/java/org/apache/iotdb/rpc/RpcUtils.java
@@ -434,12 +434,13 @@ public class RpcUtils {
return sql.substring(0, 11).equalsIgnoreCase("sql_dialect");
}
+ // Seconds are floored so that a pre-epoch time keeps a nanos part in [0,
1e9), as Timestamp needs
public static long getMilliSecond(long time, int timeFactor) {
- return time / timeFactor * 1_000;
+ return Math.floorDiv(time, timeFactor) * 1_000;
}
public static int getNanoSecond(long time, int timeFactor) {
- return (int) (time % timeFactor * (1_000_000_000 / timeFactor));
+ return Math.floorMod(time, timeFactor) * (1_000_000_000 / timeFactor);
}
public static Timestamp convertToTimestamp(long time, int timeFactor) {
diff --git
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
index 54da8cb1e70..a061bc1506c 100644
---
a/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
+++
b/iotdb-client/service-rpc/src/test/java/org/apache/iotdb/rpc/RpcUtilsTest.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.junit.Assert;
import org.junit.Test;
+import java.sql.Timestamp;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
@@ -120,4 +121,31 @@ public class RpcUtilsTest {
RpcSslUtils.configure(originProtocol);
}
}
+
+ @Test
+ public void convertToTimestampKeepsPreEpochFractions() {
+ // 1.5 seconds before the epoch in each precision
+ Assert.assertEquals(new Timestamp(-1_500L),
RpcUtils.convertToTimestamp(-1_500L, 1_000));
+ Assert.assertEquals(
+ new Timestamp(-1_500L), RpcUtils.convertToTimestamp(-1_500_000L,
1_000_000));
+ Assert.assertEquals(
+ new Timestamp(-1_500L), RpcUtils.convertToTimestamp(-1_500_000_000L,
1_000_000_000));
+
+ // 1 ns before the epoch: the second before it, plus 999,999,999 ns
+ Timestamp oneNanoBeforeEpoch = new Timestamp(-1_000L);
+ oneNanoBeforeEpoch.setNanos(999_999_999);
+ Assert.assertEquals(oneNanoBeforeEpoch, RpcUtils.convertToTimestamp(-1L,
1_000_000_000));
+
+ // Whole seconds and post-epoch values are unaffected
+ Assert.assertEquals(new Timestamp(-2_000L),
RpcUtils.convertToTimestamp(-2_000L, 1_000));
+ Assert.assertEquals(new Timestamp(1_500L),
RpcUtils.convertToTimestamp(1_500L, 1_000));
+ }
+
+ @Test
+ public void splitTimeIntoFlooredSecondsAndNonNegativeNanos() {
+ Assert.assertEquals(-2_000L, RpcUtils.getMilliSecond(-1_500L, 1_000));
+ Assert.assertEquals(500_000_000, RpcUtils.getNanoSecond(-1_500L, 1_000));
+ Assert.assertEquals(1_000L, RpcUtils.getMilliSecond(1_500L, 1_000));
+ Assert.assertEquals(500_000_000, RpcUtils.getNanoSecond(1_500L, 1_000));
+ }
}