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());
+    }
+  }
 }


Reply via email to