This is an automated email from the ASF dual-hosted git repository.
critas 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 519dec01c8d Add TimeZone header support to REST API (#17344) (#17387)
519dec01c8d is described below
commit 519dec01c8dc06ea8b2ba235b68fa7dc3f6a8833
Author: Lexert19 <[email protected]>
AuthorDate: Fri Apr 17 05:28:37 2026 +0200
Add TimeZone header support to REST API (#17344) (#17387)
* Add X-TimeZone header support to REST API (#17344)
* Support Time-Zone header and add examples for REST API
* fix test resource leaks
* copilot review
* null checks, table v1 integration tests
---------
Co-authored-by: Lexert19 <admin@DESKTOP-BN0D3J5>
---
.../main/java/org/apache/iotdb/HttpExample.java | 27 ++
.../main/java/org/apache/iotdb/HttpsExample.java | 27 ++
.../java/org/apache/iotdb/TableHttpExample.java | 30 ++
.../java/org/apache/iotdb/TableHttpsExample.java | 30 ++
.../rest/protocol/filter/AuthorizationFilter.java | 40 ++-
.../protocol/table/v1/impl/RestApiServiceImpl.java | 4 +-
.../protocol/v1/impl/GrafanaApiServiceImpl.java | 14 +-
.../rest/protocol/v1/impl/RestApiServiceImpl.java | 9 +-
.../protocol/v2/impl/GrafanaApiServiceImpl.java | 14 +-
.../rest/protocol/v2/impl/RestApiServiceImpl.java | 11 +-
.../org/apache/iotdb/db/it/IoTDBRestServiceIT.java | 323 +++++++++++++++++++++
11 files changed, 513 insertions(+), 16 deletions(-)
diff --git
a/example/rest-java-example/src/main/java/org/apache/iotdb/HttpExample.java
b/example/rest-java-example/src/main/java/org/apache/iotdb/HttpExample.java
index bccc2fd2549..98f72892b68 100644
--- a/example/rest-java-example/src/main/java/org/apache/iotdb/HttpExample.java
+++ b/example/rest-java-example/src/main/java/org/apache/iotdb/HttpExample.java
@@ -53,6 +53,7 @@ public class HttpExample {
httpExample.ping();
httpExample.insertTablet();
httpExample.query();
+ httpExample.queryWithTimeZone();
}
public void ping() {
@@ -138,4 +139,30 @@ public class HttpExample {
}
}
}
+
+ public void queryWithTimeZone() {
+ CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost = getHttpPost("http://127.0.0.1:18080/rest/v1/query");
+ httpPost.setHeader("Time-Zone", "Asia/Shanghai");
+ String sql = "{\"sql\":\"select * from root.sg25 where time <=
2026-03-28T00:00:00\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ HttpEntity responseEntity = response.getEntity();
+ String message = EntityUtils.toString(responseEntity, UTF8);
+ ObjectMapper mapper = new ObjectMapper();
+ LOGGER.info("message with time zone = {}", mapper.readValue(message,
Map.class));
+ } catch (IOException e) {
+ LOGGER.error("The query with time zone rest api failed", e);
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ LOGGER.error("Response close error", e);
+ }
+ }
+ }
}
diff --git
a/example/rest-java-example/src/main/java/org/apache/iotdb/HttpsExample.java
b/example/rest-java-example/src/main/java/org/apache/iotdb/HttpsExample.java
index a3dbbceafc7..2e5c167133b 100644
--- a/example/rest-java-example/src/main/java/org/apache/iotdb/HttpsExample.java
+++ b/example/rest-java-example/src/main/java/org/apache/iotdb/HttpsExample.java
@@ -52,6 +52,7 @@ public class HttpsExample {
httpsExample.pingHttps();
httpsExample.insertTablet();
httpsExample.query();
+ httpsExample.queryWithTimeZone();
}
public void pingHttps() {
@@ -138,4 +139,30 @@ public class HttpsExample {
}
}
}
+
+ public void queryWithTimeZone() {
+ CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost = getHttpPost("https://127.0.0.1:18080/rest/v1/query");
+ httpPost.addHeader("Time-Zone", "+05:00");
+ String sql = "{\"sql\":\"select * from root.sg25 where time <=
2026-03-28T00:00:00\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ HttpEntity responseEntity = response.getEntity();
+ String message = EntityUtils.toString(responseEntity, UTF8);
+ ObjectMapper mapper = new ObjectMapper();
+ LOGGER.info("message with time zone = {}", mapper.readValue(message,
Map.class));
+ } catch (IOException e) {
+ LOGGER.error("Https query with time zone rest api failed", e);
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ LOGGER.error("Response close error", e);
+ }
+ }
+ }
}
diff --git
a/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpExample.java
b/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpExample.java
index b9651245d02..c51ef67e6ad 100644
---
a/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpExample.java
+++
b/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpExample.java
@@ -27,6 +27,8 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -34,6 +36,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class TableHttpExample {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(TableHttpExample.class);
private static final String UTF8 = "utf-8";
@@ -50,6 +53,7 @@ public class TableHttpExample {
httpExample.nonQuery();
httpExample.insertTablet();
httpExample.query();
+ httpExample.queryWithTimeZone();
}
public void ping() {
@@ -220,4 +224,30 @@ public class TableHttpExample {
}
}
}
+
+ public void queryWithTimeZone() {
+ CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost =
getHttpPost("http://127.0.0.1:18080/rest/table/v1/query");
+ httpPost.addHeader("Time-Zone", "+05:00");
+ String sql =
+ "{\"database\":\"test\",\"sql\":\"select * from sg211 where time <=
2026-03-28T00:00:00\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ HttpEntity responseEntity = response.getEntity();
+ String message = EntityUtils.toString(responseEntity, UTF8);
+ LOGGER.info("message with time zone = {}",
JsonParser.parseString(message).getAsJsonObject());
+ } catch (IOException e) {
+ LOGGER.error("The query with time zone rest api failed", e);
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ LOGGER.error("Response close error", e);
+ }
+ }
+ }
}
diff --git
a/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpsExample.java
b/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpsExample.java
index 200dbf66fdb..181db2ae999 100644
---
a/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpsExample.java
+++
b/example/rest-java-example/src/main/java/org/apache/iotdb/TableHttpsExample.java
@@ -27,6 +27,8 @@ import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -34,6 +36,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class TableHttpsExample {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(TableHttpsExample.class);
private static final String UTF8 = "utf-8";
@@ -50,6 +53,7 @@ public class TableHttpsExample {
httpExample.nonQuery();
httpExample.insertTablet();
httpExample.query();
+ httpExample.queryWithTimeZone();
}
public void ping() {
@@ -220,4 +224,30 @@ public class TableHttpsExample {
}
}
}
+
+ public void queryWithTimeZone() {
+ CloseableHttpClient httpClient = SSLClient.getInstance().getHttpClient();
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost =
getHttpPost("https://127.0.0.1:18080/rest/table/v1/query");
+ httpPost.addHeader("Time-Zone", "+05:00");
+ String sql =
+ "{\"database\":\"test\",\"sql\":\"select * from sg211 where time <=
2026-03-28T00:00:00\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ HttpEntity responseEntity = response.getEntity();
+ String message = EntityUtils.toString(responseEntity, UTF8);
+ LOGGER.info("message with time zone = {}",
JsonParser.parseString(message).getAsJsonObject());
+ } catch (IOException e) {
+ LOGGER.error("The query with time zone rest api failed", e);
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ LOGGER.error("Response close error", e);
+ }
+ }
+ }
}
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/filter/AuthorizationFilter.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/filter/AuthorizationFilter.java
index a62a402e3d2..d973933260c 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/filter/AuthorizationFilter.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/filter/AuthorizationFilter.java
@@ -39,6 +39,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
+import java.time.DateTimeException;
import java.time.ZoneId;
import java.util.Base64;
import java.util.UUID;
@@ -88,6 +89,12 @@ public class AuthorizationFilter implements
ContainerRequestFilter, ContainerRes
if (user == null) {
return;
}
+
+ ZoneId zoneId = resolveTimeZone(containerRequestContext);
+ if (zoneId == null) {
+ return;
+ }
+
String sessionid = UUID.randomUUID().toString();
if (SESSION_MANAGER.getCurrSession() == null) {
RestClientSession restClientSession = new RestClientSession(sessionid);
@@ -97,7 +104,7 @@ public class AuthorizationFilter implements
ContainerRequestFilter, ContainerRes
SESSION_MANAGER.getCurrSession(),
user.getUserId(),
user.getUsername(),
- ZoneId.systemDefault(),
+ zoneId,
IoTDBConstant.ClientVersion.V_1_0);
}
BasicSecurityContext basicSecurityContext =
@@ -147,6 +154,37 @@ public class AuthorizationFilter implements
ContainerRequestFilter, ContainerRes
return user;
}
+ /**
+ * Resolves the Time-Zone header from the request.
+ *
+ * @param requestContext the incoming HTTP request
+ * @return the resolved ZoneId, or {@code null} if the header is invalid
(the request is aborted)
+ */
+ private ZoneId resolveTimeZone(ContainerRequestContext requestContext) {
+ String timeZoneHeader = requestContext.getHeaderString("Time-Zone");
+ if (timeZoneHeader == null) {
+ return ZoneId.systemDefault();
+ }
+ timeZoneHeader = timeZoneHeader.trim();
+ if (timeZoneHeader.isEmpty()) {
+ return ZoneId.systemDefault();
+ }
+ try {
+ return ZoneId.of(timeZoneHeader);
+ } catch (DateTimeException e) {
+ Response resp =
+ Response.status(Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON)
+ .entity(
+ new ExecutionStatus()
+ .code(TSStatusCode.ILLEGAL_PARAMETER.getStatusCode())
+ .message("Invalid time zone: " + timeZoneHeader))
+ .build();
+ requestContext.abortWith(resp);
+ return null;
+ }
+ }
+
@Override
public void filter(
ContainerRequestContext requestContext, ContainerResponseContext
responseContext)
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/table/v1/impl/RestApiServiceImpl.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/table/v1/impl/RestApiServiceImpl.java
index b35e9d27f59..e66cb88a6e1 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/table/v1/impl/RestApiServiceImpl.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/table/v1/impl/RestApiServiceImpl.java
@@ -51,7 +51,6 @@ import org.apache.iotdb.rpc.TSStatusCode;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
-import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
@@ -287,7 +286,8 @@ public class RestApiServiceImpl extends RestApiService {
}
clientSession.setSqlDialect(IClientSession.SqlDialect.TABLE);
- return relationSqlParser.createStatement(sql.getSql(),
ZoneId.systemDefault(), clientSession);
+ return relationSqlParser.createStatement(
+ sql.getSql(), clientSession.getZoneId(), clientSession);
}
private Response validateStatement(Statement statement, boolean userQuery) {
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/GrafanaApiServiceImpl.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/GrafanaApiServiceImpl.java
index 0db4cd06c66..c601bc5107b 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/GrafanaApiServiceImpl.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/GrafanaApiServiceImpl.java
@@ -21,6 +21,7 @@ import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.protocol.session.IClientSession;
import org.apache.iotdb.db.protocol.session.SessionManager;
import org.apache.iotdb.db.queryengine.plan.Coordinator;
import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
@@ -91,8 +92,9 @@ public class GrafanaApiServiceImpl extends GrafanaApiService {
try {
RequestValidationHandler.validateSQL(sql);
- Statement statement =
- StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql.getSql(),
zoneId);
if (!(statement instanceof ShowStatement) && !(statement instanceof
QueryStatement)) {
return Response.ok()
.entity(
@@ -168,7 +170,9 @@ public class GrafanaApiServiceImpl extends
GrafanaApiService {
sql += " " + expressionRequest.getControl();
}
- Statement statement = StatementGenerator.createStatement(sql,
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql, zoneId);
Response response = authorizationHandler.checkAuthority(securityContext,
statement);
if (response != null) {
@@ -232,7 +236,9 @@ public class GrafanaApiServiceImpl extends
GrafanaApiService {
// TODO: necessary to create a partial path?
PartialPath path = new PartialPath(Joiner.on(".").join(requestBody));
String sql = "show child paths " + path;
- Statement statement = StatementGenerator.createStatement(sql,
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql, zoneId);
Response response =
authorizationHandler.checkAuthority(securityContext, statement);
if (response != null) {
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/RestApiServiceImpl.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/RestApiServiceImpl.java
index 329ac47034b..d8b5328fe00 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/RestApiServiceImpl.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v1/impl/RestApiServiceImpl.java
@@ -20,6 +20,7 @@ package org.apache.iotdb.rest.protocol.v1.impl;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.conf.rest.IoTDBRestServiceDescriptor;
+import org.apache.iotdb.db.protocol.session.IClientSession;
import org.apache.iotdb.db.protocol.session.SessionManager;
import org.apache.iotdb.db.protocol.thrift.OperationType;
import org.apache.iotdb.db.queryengine.plan.Coordinator;
@@ -87,7 +88,9 @@ public class RestApiServiceImpl extends RestApiService {
Statement statement = null;
try {
RequestValidationHandler.validateSQL(sql);
- statement = StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
if (statement == null) {
return Response.ok()
.entity(
@@ -177,7 +180,9 @@ public class RestApiServiceImpl extends RestApiService {
Statement statement = null;
try {
RequestValidationHandler.validateSQL(sql);
- statement = StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
if (statement == null) {
return Response.ok()
.entity(
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/GrafanaApiServiceImpl.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/GrafanaApiServiceImpl.java
index 5b120b9c1d7..3842625eee4 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/GrafanaApiServiceImpl.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/GrafanaApiServiceImpl.java
@@ -21,6 +21,7 @@ import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.protocol.session.IClientSession;
import org.apache.iotdb.db.protocol.session.SessionManager;
import org.apache.iotdb.db.queryengine.plan.Coordinator;
import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
@@ -91,8 +92,9 @@ public class GrafanaApiServiceImpl extends GrafanaApiService {
try {
RequestValidationHandler.validateSQL(sql);
- Statement statement =
- StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql.getSql(),
zoneId);
if (!(statement instanceof ShowStatement) && !(statement instanceof
QueryStatement)) {
return Response.ok()
.entity(
@@ -168,7 +170,9 @@ public class GrafanaApiServiceImpl extends
GrafanaApiService {
sql += " " + expressionRequest.getControl();
}
- Statement statement = StatementGenerator.createStatement(sql,
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql, zoneId);
Response response = authorizationHandler.checkAuthority(securityContext,
statement);
if (response != null) {
@@ -232,7 +236,9 @@ public class GrafanaApiServiceImpl extends
GrafanaApiService {
// TODO: necessary to create a PartialPath
PartialPath path = new PartialPath(Joiner.on(".").join(requestBody));
String sql = "show child paths " + path;
- Statement statement = StatementGenerator.createStatement(sql,
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ Statement statement = StatementGenerator.createStatement(sql, zoneId);
Response response =
authorizationHandler.checkAuthority(securityContext, statement);
if (response != null) {
diff --git
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/RestApiServiceImpl.java
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/RestApiServiceImpl.java
index e05929c6a63..e0ac81ee92f 100644
---
a/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/RestApiServiceImpl.java
+++
b/external-service-impl/rest/src/main/java/org/apache/iotdb/rest/protocol/v2/impl/RestApiServiceImpl.java
@@ -274,7 +274,9 @@ public class RestApiServiceImpl extends RestApiService {
boolean finish = false;
try {
RequestValidationHandler.validateSQL(sql);
- statement = StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
if (statement == null) {
return Response.ok()
.entity(
@@ -314,9 +316,10 @@ public class RestApiServiceImpl extends RestApiService {
return
Response.ok().entity(ExceptionHandler.tryCatchException(e)).build();
} finally {
long costTime = System.nanoTime() - startTime;
- if (statement != null)
+ if (statement != null) {
CommonUtils.addStatementExecutionLatency(
OperationType.EXECUTE_NON_QUERY_PLAN, statement.getType().name(),
costTime);
+ }
if (queryId != null) {
if (finish) {
long executionTime = COORDINATOR.getTotalExecutionTime(queryId);
@@ -336,7 +339,9 @@ public class RestApiServiceImpl extends RestApiService {
boolean finish = false;
try {
RequestValidationHandler.validateSQL(sql);
- statement = StatementGenerator.createStatement(sql.getSql(),
ZoneId.systemDefault());
+ IClientSession session = SESSION_MANAGER.getCurrSession();
+ ZoneId zoneId = (session != null) ? session.getZoneId() :
ZoneId.systemDefault();
+ statement = StatementGenerator.createStatement(sql.getSql(), zoneId);
if (statement == null) {
return Response.ok()
diff --git
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBRestServiceIT.java
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBRestServiceIT.java
index eb2573c7f84..be528cce786 100644
---
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBRestServiceIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBRestServiceIT.java
@@ -27,8 +27,10 @@ import org.apache.iotdb.itbase.category.ClusterIT;
import org.apache.iotdb.itbase.category.LocalStandaloneIT;
import org.apache.iotdb.itbase.category.RemoteIT;
import org.apache.iotdb.itbase.env.BaseEnv;
+import org.apache.iotdb.rpc.TSStatusCode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.http.HttpEntity;
@@ -53,6 +55,8 @@ import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
@@ -476,6 +480,12 @@ public class IoTDBRestServiceIT {
listUser(httpClient);
selectCount(httpClient);
selectLast(httpClient);
+ queryWithValidTimeZoneHeader(httpClient);
+ nonQueryWithValidTimeZoneHeader(httpClient);
+ queryWithInvalidTimeZoneHeader(httpClient);
+ nonQueryWithValidTimeZoneHeaderTableV1(httpClient);
+ queryWithValidTimeZoneHeaderTableV1(httpClient);
+ queryWithInvalidTimeZoneHeaderTableV1(httpClient);
queryV2(httpClient);
selectFastLast(httpClient);
@@ -497,6 +507,9 @@ public class IoTDBRestServiceIT {
listUserV2(httpClient);
selectCountV2(httpClient);
selectLastV2(httpClient);
+ queryWithValidTimeZoneHeaderV2(httpClient);
+ nonQueryWithValidTimeZoneHeaderV2(httpClient);
+ queryWithInvalidTimeZoneHeaderV2(httpClient);
perData(httpClient);
List<String> insertTablet_right_json_list = new ArrayList<>();
List<String> insertTablet_error_json_list = new ArrayList<>();
@@ -2514,4 +2527,314 @@ public class IoTDBRestServiceIT {
}
}
}
+
+ public void queryWithValidTimeZoneHeader(CloseableHttpClient httpClient) {
+ CloseableHttpResponse response = null;
+ try {
+ long expectedTimestamp =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ HttpPost insertPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/nonQuery");
+ String insertSql =
+ String.format(
+ "{\"sql\":\"INSERT INTO root.sg_tz.d2(timestamp, s3) VALUES(%d,
10.5)\"}",
+ expectedTimestamp);
+ insertPost.setEntity(new StringEntity(insertSql,
Charset.defaultCharset()));
+ try (CloseableHttpResponse insertResponse =
httpClient.execute(insertPost)) {
+ assertEquals(200, insertResponse.getStatusLine().getStatusCode());
+ }
+ HttpPost httpPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/query");
+ httpPost.setHeader("Time-Zone", "+05:00");
+ String sql =
+ "{\"sql\":\"SELECT count(s3) FROM root.sg_tz.d2 GROUP BY
([2026-03-28T00:00:00, 2026-03-29T00:00:00), 1d)\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(), "utf-8");
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ assertTrue(result.has("timestamps"));
+ assertTrue(result.getAsJsonArray("timestamps").size() > 0);
+ assertEquals(expectedTimestamp,
result.getAsJsonArray("timestamps").get(0).getAsLong());
+ JsonArray values = result.getAsJsonArray("values");
+ int countResult = values.get(0).getAsJsonArray().get(0).getAsInt();
+ assertEquals(1, countResult);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ public void nonQueryWithValidTimeZoneHeader(CloseableHttpClient httpClient) {
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost createPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/nonQuery");
+ nonQuery(
+ httpClient,
+ "{\"sql\":\"CREATE TIMESERIES root.sg_tz.d1.s1 WITH
DATATYPE=INT32\"}",
+ createPost);
+
+ HttpPost insertPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/nonQuery");
+ insertPost.setHeader("Time-Zone", "+05:00");
+ nonQuery(
+ httpClient,
+ "{\"sql\":\"INSERT INTO root.sg_tz.d1(time, s1) VALUES
(2026-03-28T00:00:00, 123)\"}",
+ insertPost);
+
+ HttpPost queryPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/query");
+ queryPost.setEntity(
+ new StringEntity("{\"sql\":\"SELECT s1 FROM root.sg_tz.d1\"}",
StandardCharsets.UTF_8));
+ response = httpClient.execute(queryPost);
+ String message = EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8);
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ long expected =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ assertEquals(expected,
result.getAsJsonArray("timestamps").get(0).getAsLong());
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ public void queryWithValidTimeZoneHeaderV2(CloseableHttpClient httpClient) {
+ CloseableHttpResponse response = null;
+ try {
+ long expectedTimestamp =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ HttpPost insertPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/nonQuery");
+ String insertSql =
+ String.format(
+ "{\"sql\":\"INSERT INTO root.sg_tz.d3(timestamp, s3) VALUES(%d,
10.5)\"}",
+ expectedTimestamp);
+ insertPost.setEntity(new StringEntity(insertSql,
Charset.defaultCharset()));
+ try (CloseableHttpResponse insertResponse =
httpClient.execute(insertPost)) {
+ assertEquals(200, insertResponse.getStatusLine().getStatusCode());
+ }
+ HttpPost httpPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v2/query");
+ httpPost.setHeader("Time-Zone", "+05:00");
+ String sql =
+ "{\"sql\":\"SELECT count(s3) FROM root.sg_tz.d3 GROUP BY
([2026-03-28T00:00:00, 2026-03-29T00:00:00), 1d)\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(), "utf-8");
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ assertTrue(result.has("timestamps"));
+ assertTrue(result.getAsJsonArray("timestamps").size() > 0);
+ assertEquals(expectedTimestamp,
result.getAsJsonArray("timestamps").get(0).getAsLong());
+ JsonArray values = result.getAsJsonArray("values");
+ int countResult = values.get(0).getAsJsonArray().get(0).getAsInt();
+ assertEquals(1, countResult);
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ public void nonQueryWithValidTimeZoneHeaderV2(CloseableHttpClient
httpClient) {
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost createPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v2/nonQuery");
+ nonQuery(
+ httpClient,
+ "{\"sql\":\"CREATE TIMESERIES root.sg_tz.d2.s1 WITH
DATATYPE=INT32\"}",
+ createPost);
+
+ HttpPost insertPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v2/nonQuery");
+ insertPost.setHeader("Time-Zone", "+05:00");
+ nonQuery(
+ httpClient,
+ "{\"sql\":\"INSERT INTO root.sg_tz.d2(time, s1) VALUES
(2026-03-28T00:00:00, 123)\"}",
+ insertPost);
+
+ HttpPost queryPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v2/query");
+ queryPost.setEntity(
+ new StringEntity("{\"sql\":\"SELECT s1 FROM root.sg_tz.d2\"}",
StandardCharsets.UTF_8));
+ response = httpClient.execute(queryPost);
+ String message = EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8);
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ long expected =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ assertEquals(expected,
result.getAsJsonArray("timestamps").get(0).getAsLong());
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ public void queryWithInvalidTimeZoneHeader(CloseableHttpClient httpClient) {
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v1/query");
+ httpPost.setHeader("Time-Zone", "Invalid/Zone");
+ String sql = "{\"sql\":\"SELECT s3 FROM root.sg25\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(), "utf-8");
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ assertEquals(TSStatusCode.ILLEGAL_PARAMETER.getStatusCode(),
result.get("code").getAsInt());
+ assertTrue(result.get("message").getAsString().contains("Invalid time
zone"));
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ public void queryWithInvalidTimeZoneHeaderV2(CloseableHttpClient httpClient)
{
+ CloseableHttpResponse response = null;
+ try {
+ HttpPost httpPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/v2/query");
+ httpPost.setHeader("Time-Zone", "Invalid/Zone");
+ String sql = "{\"sql\":\"SELECT s3 FROM root.sg25\"}";
+ httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+ response = httpClient.execute(httpPost);
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(), "utf-8");
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ assertEquals(TSStatusCode.ILLEGAL_PARAMETER.getStatusCode(),
result.get("code").getAsInt());
+ assertTrue(result.get("message").getAsString().contains("Invalid time
zone"));
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ } finally {
+ try {
+ if (response != null) {
+ response.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+ }
+
+ private void nonQueryWithValidTimeZoneHeaderTableV1(CloseableHttpClient
httpClient) {
+ nonQuery(
+ httpClient,
+ "{\"database\":\"\", \"sql\":\"CREATE DATABASE table_tz\"}",
+ getHttpPost("http://127.0.0.1:" + port + "/rest/table/v1/nonQuery"));
+ nonQuery(
+ httpClient,
+ "{\"database\":\"table_tz\", \"sql\":\"CREATE TABLE d1 (s1 INT32)\"}",
+ getHttpPost("http://127.0.0.1:" + port + "/rest/table/v1/nonQuery"));
+
+ HttpPost insertPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/table/v1/nonQuery");
+ insertPost.setHeader("Time-Zone", "+05:00");
+ nonQuery(
+ httpClient,
+ "{\"database\":\"table_tz\", \"sql\":\"INSERT INTO d1(time, s1) VALUES
('2026-03-28T00:00:00', 123)\"}",
+ insertPost);
+
+ HttpPost queryPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/table/v1/query");
+ queryPost.setEntity(
+ new StringEntity(
+ "{\"database\":\"table_tz\", \"sql\":\"SELECT time, s1 FROM d1\"}",
+ StandardCharsets.UTF_8));
+ try (CloseableHttpResponse response = httpClient.execute(queryPost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8);
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ long actualTimestamp =
+
result.getAsJsonArray("values").get(0).getAsJsonArray().get(0).getAsLong();
+ long expectedTimestamp =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ assertEquals(expectedTimestamp, actualTimestamp);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void queryWithValidTimeZoneHeaderTableV1(CloseableHttpClient
httpClient) {
+ HttpPost preparePost = getHttpPost("http://127.0.0.1:" + port +
"/rest/table/v1/nonQuery");
+ nonQuery(httpClient, "{\"database\":\"\", \"sql\":\"CREATE DATABASE
table_tz2\"}", preparePost);
+ nonQuery(
+ httpClient,
+ "{\"database\":\"table_tz2\", \"sql\":\"CREATE TABLE d2 (s1 INT32)\"}",
+ preparePost);
+
+ long absoluteTimestamp =
+ ZonedDateTime.of(2026, 3, 28, 0, 0, 0, 0,
ZoneId.of("+05:00")).toInstant().toEpochMilli();
+ String insertSql =
+ String.format(
+ "{\"database\":\"table_tz2\", \"sql\":\"INSERT INTO d2(time, s1)
VALUES (%d, 456)\"}",
+ absoluteTimestamp);
+ nonQuery(httpClient, insertSql, preparePost);
+
+ HttpPost queryPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/table/v1/query");
+ queryPost.setHeader("Time-Zone", "+05:00");
+ String sql =
+ "{\"database\":\"table_tz2\", \"sql\":\"SELECT COUNT(s1) FROM d2 WHERE
time = 2026-03-28T00:00:00\"}";
+ queryPost.setEntity(new StringEntity(sql, StandardCharsets.UTF_8));
+ try (CloseableHttpResponse response = httpClient.execute(queryPost)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8);
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ long actualCount =
result.getAsJsonArray("values").get(0).getAsJsonArray().get(0).getAsLong();
+ assertEquals(1, actualCount);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void queryWithInvalidTimeZoneHeaderTableV1(CloseableHttpClient
httpClient) {
+ HttpPost queryPost = getHttpPost("http://127.0.0.1:" + port +
"/rest/table/v1/query");
+ queryPost.setHeader("Time-Zone", "Invalid/Zone");
+ String sql = "{\"database\":\"table_tz\", \"sql\":\"SELECT 1\"}";
+ queryPost.setEntity(new StringEntity(sql, StandardCharsets.UTF_8));
+ try (CloseableHttpResponse response = httpClient.execute(queryPost)) {
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ String message = EntityUtils.toString(response.getEntity(), "utf-8");
+ JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+ assertEquals(TSStatusCode.ILLEGAL_PARAMETER.getStatusCode(),
result.get("code").getAsInt());
+ assertTrue(result.get("message").getAsString().contains("Invalid time
zone"));
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ }
}