This is an automated email from the ASF dual-hosted git repository.

Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 910b741ba32 [fix](exec) Return DATETIME as naive Arrow timestamp 
(#10492) (#66873)
910b741ba32 is described below

commit 910b741ba329efb8f406fd13e87520e9bed95501
Author: Mryange <[email protected]>
AuthorDate: Wed Aug 19 16:21:57 2026 +0800

    [fix](exec) Return DATETIME as naive Arrow timestamp (#10492) (#66873)
    
    Arrow Flight SQL exposed Doris DATETIME values as timezone-aware Arrow
    timestamps, causing clients to interpret wall-clock values as instants
    and apply unwanted timezone conversion. The result schema now represents
    DATETIME as a timezone-naive timestamp while preserving timezone-aware
    TIMESTAMPTZ behavior. Arrow conversion unit tests and Arrow Flight SQL
    regression coverage were added.
    
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/exec/operator/result_sink_operator.cpp      |  6 +-
 be/src/format/arrow/arrow_row_batch.cpp            | 42 ++++++++----
 be/src/format/arrow/arrow_row_batch.h              |  7 +-
 .../data_type_serde/data_type_serde_arrow_test.cpp | 75 ++++++++++++++++++++++
 .../service/arrowflight/FlightSqlSchemaHelper.java | 14 ++--
 .../data/arrow_flight_sql_p0/test_select.out       |  6 +-
 .../suites/arrow_flight_sql_p0/test_select.groovy  | 65 +++++++++++++++++--
 7 files changed, 180 insertions(+), 35 deletions(-)

diff --git a/be/src/exec/operator/result_sink_operator.cpp 
b/be/src/exec/operator/result_sink_operator.cpp
index 021ffb60983..12611b36aaa 100644
--- a/be/src/exec/operator/result_sink_operator.cpp
+++ b/be/src/exec/operator/result_sink_operator.cpp
@@ -57,7 +57,8 @@ Status ResultSinkLocalState::init(RuntimeState* state, 
LocalSinkStateInfo& info)
         std::shared_ptr<arrow::Schema> arrow_schema;
         if (p._sink_type == TResultSinkType::ARROW_FLIGHT_PROTOCOL) {
             
RETURN_IF_ERROR(get_arrow_schema_from_expr_ctxs(_output_vexpr_ctxs, 
&arrow_schema,
-                                                            
state->timezone()));
+                                                            state->timezone(),
+                                                            
/*datetime_naive=*/true));
         }
         VLOG_DEBUG << "create sender in INIT with instance id " << 
fragment_instance_id;
         RETURN_IF_ERROR(state->exec_env()->result_mgr()->create_sender(
@@ -122,7 +123,8 @@ Status ResultSinkOperatorX::prepare(RuntimeState* state) {
         std::shared_ptr<arrow::Schema> arrow_schema;
         if (_sink_type == TResultSinkType::ARROW_FLIGHT_PROTOCOL) {
             
RETURN_IF_ERROR(get_arrow_schema_from_expr_ctxs(_output_vexpr_ctxs, 
&arrow_schema,
-                                                            
state->timezone()));
+                                                            state->timezone(),
+                                                            
/*datetime_naive=*/true));
         }
         VLOG_DEBUG << "create sender in prepare with query id " << 
state->query_id();
         RETURN_IF_ERROR(state->exec_env()->result_mgr()->create_sender(
diff --git a/be/src/format/arrow/arrow_row_batch.cpp 
b/be/src/format/arrow/arrow_row_batch.cpp
index 9c8e94e10c4..4f80fb042ea 100644
--- a/be/src/format/arrow/arrow_row_batch.cpp
+++ b/be/src/format/arrow/arrow_row_batch.cpp
@@ -49,8 +49,8 @@
 namespace doris {
 
 Status convert_to_arrow_type(const DataTypePtr& origin_type,
-                             std::shared_ptr<arrow::DataType>* result,
-                             const std::string& timezone) {
+                             std::shared_ptr<arrow::DataType>* result, const 
std::string& timezone,
+                             bool datetime_naive) {
     auto type = get_serialized_type(origin_type);
     switch (type->get_primitive_type()) {
     case TYPE_NULL:
@@ -97,17 +97,27 @@ Status convert_to_arrow_type(const DataTypePtr& origin_type,
     case TYPE_DATEV2:
         *result = std::make_shared<arrow::Date32Type>();
         break;
-    // TODO: maybe need to distinguish TYPE_DATETIME and TYPE_TIMESTAMPTZ
     case TYPE_TIMESTAMPTZ:
-    case TYPE_DATETIMEV2:
+    case TYPE_DATETIMEV2: {
+        arrow::TimeUnit::type time_unit;
         if (type->get_scale() > 3) {
-            *result = 
std::make_shared<arrow::TimestampType>(arrow::TimeUnit::MICRO, timezone);
+            time_unit = arrow::TimeUnit::MICRO;
         } else if (type->get_scale() > 0) {
-            *result = 
std::make_shared<arrow::TimestampType>(arrow::TimeUnit::MILLI, timezone);
+            time_unit = arrow::TimeUnit::MILLI;
         } else {
-            *result = 
std::make_shared<arrow::TimestampType>(arrow::TimeUnit::SECOND, timezone);
+            time_unit = arrow::TimeUnit::SECOND;
+        }
+        // Doris DATETIMEV2 represents a wall-clock value without a timezone. 
Arrow Flight
+        // exposes it as a timezone-naive timestamp so clients do not 
interpret it as an instant.
+        // This option only changes the DATETIMEV2 output schema. TIMESTAMPTZ 
remains timezone-aware,
+        // and Arrow-to-Doris conversions are unaffected.
+        if (type->get_primitive_type() == TYPE_DATETIMEV2 && datetime_naive) {
+            *result = std::make_shared<arrow::TimestampType>(time_unit);
+        } else {
+            *result = std::make_shared<arrow::TimestampType>(time_unit, 
timezone);
         }
         break;
+    }
     case TYPE_DECIMALV2:
     case TYPE_DECIMAL32:
     case TYPE_DECIMAL64:
@@ -123,7 +133,8 @@ Status convert_to_arrow_type(const DataTypePtr& origin_type,
     case TYPE_ARRAY: {
         const auto* type_arr = assert_cast<const 
DataTypeArray*>(remove_nullable(type).get());
         std::shared_ptr<arrow::DataType> item_type;
-        RETURN_IF_ERROR(convert_to_arrow_type(type_arr->get_nested_type(), 
&item_type, timezone));
+        RETURN_IF_ERROR(convert_to_arrow_type(type_arr->get_nested_type(), 
&item_type, timezone,
+                                              datetime_naive));
         *result = std::make_shared<arrow::ListType>(item_type);
         break;
     }
@@ -131,8 +142,10 @@ Status convert_to_arrow_type(const DataTypePtr& 
origin_type,
         const auto* type_map = assert_cast<const 
DataTypeMap*>(remove_nullable(type).get());
         std::shared_ptr<arrow::DataType> key_type;
         std::shared_ptr<arrow::DataType> val_type;
-        RETURN_IF_ERROR(convert_to_arrow_type(type_map->get_key_type(), 
&key_type, timezone));
-        RETURN_IF_ERROR(convert_to_arrow_type(type_map->get_value_type(), 
&val_type, timezone));
+        RETURN_IF_ERROR(convert_to_arrow_type(type_map->get_key_type(), 
&key_type, timezone,
+                                              datetime_naive));
+        RETURN_IF_ERROR(convert_to_arrow_type(type_map->get_value_type(), 
&val_type, timezone,
+                                              datetime_naive));
         *result = std::make_shared<arrow::MapType>(key_type, val_type);
         break;
     }
@@ -141,8 +154,8 @@ Status convert_to_arrow_type(const DataTypePtr& origin_type,
         std::vector<std::shared_ptr<arrow::Field>> fields;
         for (size_t i = 0; i < type_struct->get_elements().size(); i++) {
             std::shared_ptr<arrow::DataType> field_type;
-            RETURN_IF_ERROR(
-                    convert_to_arrow_type(type_struct->get_element(i), 
&field_type, timezone));
+            RETURN_IF_ERROR(convert_to_arrow_type(type_struct->get_element(i), 
&field_type,
+                                                  timezone, datetime_naive));
             fields.push_back(
                     
std::make_shared<arrow::Field>(type_struct->get_element_name(i), field_type,
                                                    
type_struct->get_element(i)->is_nullable()));
@@ -206,12 +219,13 @@ Status get_arrow_schema_from_block(const Block& block, 
std::shared_ptr<arrow::Sc
 
 Status get_arrow_schema_from_expr_ctxs(const VExprContextSPtrs& 
output_vexpr_ctxs,
                                        std::shared_ptr<arrow::Schema>* result,
-                                       const std::string& timezone) {
+                                       const std::string& timezone, bool 
datetime_naive) {
     std::vector<std::shared_ptr<arrow::Field>> fields;
     for (int i = 0; i < output_vexpr_ctxs.size(); i++) {
         std::shared_ptr<arrow::DataType> arrow_type;
         auto root_expr = output_vexpr_ctxs.at(i)->root();
-        RETURN_IF_ERROR(convert_to_arrow_type(root_expr->data_type(), 
&arrow_type, timezone));
+        RETURN_IF_ERROR(convert_to_arrow_type(root_expr->data_type(), 
&arrow_type, timezone,
+                                              datetime_naive));
         auto field_name = root_expr->is_slot_ref() && 
!root_expr->expr_label().empty()
                                   ? root_expr->expr_label()
                                   : fmt::format("{}_{}", 
root_expr->data_type()->get_name(), i);
diff --git a/be/src/format/arrow/arrow_row_batch.h 
b/be/src/format/arrow/arrow_row_batch.h
index e7b77ed707b..e5ddd18fae6 100644
--- a/be/src/format/arrow/arrow_row_batch.h
+++ b/be/src/format/arrow/arrow_row_batch.h
@@ -43,8 +43,11 @@ constexpr size_t MAX_ARROW_UTF8 = (1ULL << 31); // 2G
 
 class RowDescriptor;
 
+// datetime_naive only controls how Doris DATETIMEV2 is represented in the 
output Arrow schema.
+// When enabled, DATETIMEV2 is mapped to a timestamp without a timezone to 
preserve its wall-clock
+// semantics. TIMESTAMPTZ remains timezone-aware, and Arrow-to-Doris 
conversions are unaffected.
 Status convert_to_arrow_type(const DataTypePtr& type, 
std::shared_ptr<arrow::DataType>* result,
-                             const std::string& timezone);
+                             const std::string& timezone, bool datetime_naive 
= false);
 
 std::shared_ptr<arrow::Field> create_arrow_field_with_metadata(
         const std::string& field_name, const std::shared_ptr<arrow::DataType>& 
arrow_type,
@@ -55,7 +58,7 @@ Status get_arrow_schema_from_block(const Block& block, 
std::shared_ptr<arrow::Sc
 
 Status get_arrow_schema_from_expr_ctxs(const VExprContextSPtrs& 
output_vexpr_ctxs,
                                        std::shared_ptr<arrow::Schema>* result,
-                                       const std::string& timezone);
+                                       const std::string& timezone, bool 
datetime_naive = false);
 
 Status serialize_record_batch(const arrow::RecordBatch& record_batch, 
std::string* result);
 
diff --git a/be/test/core/data_type_serde/data_type_serde_arrow_test.cpp 
b/be/test/core/data_type_serde/data_type_serde_arrow_test.cpp
index a80ab6f885d..2867415f9a2 100644
--- a/be/test/core/data_type_serde/data_type_serde_arrow_test.cpp
+++ b/be/test/core/data_type_serde/data_type_serde_arrow_test.cpp
@@ -36,6 +36,7 @@
 #include <gtest/gtest-test-part.h>
 #include <gtest/gtest.h>
 
+#include <chrono>
 #include <cmath>
 #include <cstdint>
 #include <cstring>
@@ -70,6 +71,7 @@
 #include "core/data_type/data_type_quantilestate.h"
 #include "core/data_type/data_type_string.h"
 #include "core/data_type/data_type_struct.h"
+#include "core/data_type/data_type_timestamptz.h"
 #include "core/data_type/define_primitive_type.h"
 #include "core/field.h"
 #include "core/types.h"
@@ -654,4 +656,77 @@ TEST(DataTypeSerDeArrowTest, BlockConverterTest) {
     block_converter_test(cols, 7, false);
 }
 
+TEST(DataTypeSerDeArrowTest, ConvertDateTimeV2ToNaiveArrowType) {
+    const auto datetime_type = std::make_shared<DataTypeDateTimeV2>(6);
+    std::shared_ptr<arrow::DataType> arrow_type;
+
+    auto status = convert_to_arrow_type(datetime_type, &arrow_type, 
"Asia/Shanghai");
+    ASSERT_TRUE(status.ok()) << status;
+    auto timestamp_type = 
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+    EXPECT_EQ(arrow::TimeUnit::MICRO, timestamp_type->unit());
+    EXPECT_EQ("Asia/Shanghai", timestamp_type->timezone());
+
+    status = convert_to_arrow_type(datetime_type, &arrow_type, 
"Asia/Shanghai", true);
+    ASSERT_TRUE(status.ok()) << status;
+    timestamp_type = 
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+    EXPECT_EQ(arrow::TimeUnit::MICRO, timestamp_type->unit());
+    EXPECT_TRUE(timestamp_type->timezone().empty());
+
+    const auto timestamptz_type = std::make_shared<DataTypeTimeStampTz>(6);
+    status = convert_to_arrow_type(timestamptz_type, &arrow_type, 
"Asia/Shanghai", true);
+    ASSERT_TRUE(status.ok()) << status;
+    timestamp_type = 
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+    EXPECT_EQ(arrow::TimeUnit::MICRO, timestamp_type->unit());
+    EXPECT_EQ("Asia/Shanghai", timestamp_type->timezone());
+
+    const auto array_type = std::make_shared<DataTypeArray>(datetime_type);
+    status = convert_to_arrow_type(array_type, &arrow_type, "Asia/Shanghai", 
true);
+    ASSERT_TRUE(status.ok()) << status;
+    const auto list_type = 
std::static_pointer_cast<arrow::ListType>(arrow_type);
+    timestamp_type = 
std::static_pointer_cast<arrow::TimestampType>(list_type->value_type());
+    EXPECT_TRUE(timestamp_type->timezone().empty());
+}
+
+TEST(DataTypeSerDeArrowTest, DateTimeV2ArrowEncodingFollowsSchemaTimezone) {
+    auto datetime_column = ColumnVector<TYPE_DATETIMEV2>::create();
+    DateV2Value<DateTimeV2ValueType> datetime_value;
+    datetime_value.unchecked_set_time(2026, 7, 2, 15, 0, 0, 123456);
+    
datetime_column->insert(Field::create_field<TYPE_DATETIMEV2>(datetime_value));
+
+    auto datetime_type = std::make_shared<DataTypeDateTimeV2>(6);
+    Block block;
+    block.insert(ColumnWithTypeAndName(datetime_column->get_ptr(), 
datetime_type, "ts"));
+
+    const auto utc_plus_eight = cctz::fixed_time_zone(std::chrono::hours(8));
+    auto timezone_schema = arrow::schema(
+            {arrow::field("ts", arrow::timestamp(arrow::TimeUnit::MICRO, 
"+08:00"), false)});
+    std::shared_ptr<arrow::RecordBatch> timezone_batch;
+    auto status = convert_to_arrow_batch(block, timezone_schema, 
arrow::default_memory_pool(),
+                                         &timezone_batch, utc_plus_eight);
+    ASSERT_TRUE(status.ok()) << status;
+
+    const auto timezone_type = std::static_pointer_cast<arrow::TimestampType>(
+            timezone_batch->schema()->field(0)->type());
+    EXPECT_EQ("+08:00", timezone_type->timezone());
+    const auto timezone_array =
+            
std::static_pointer_cast<arrow::TimestampArray>(timezone_batch->column(0));
+    // 2026-07-02 15:00:00.123456+08:00 is 2026-07-02 07:00:00.123456 UTC.
+    EXPECT_EQ(1782975600123456, timezone_array->Value(0));
+
+    auto naive_schema =
+            arrow::schema({arrow::field("ts", 
arrow::timestamp(arrow::TimeUnit::MICRO), false)});
+    std::shared_ptr<arrow::RecordBatch> naive_batch;
+    status = convert_to_arrow_batch(block, naive_schema, 
arrow::default_memory_pool(), &naive_batch,
+                                    utc_plus_eight);
+    ASSERT_TRUE(status.ok()) << status;
+
+    const auto naive_type =
+            
std::static_pointer_cast<arrow::TimestampType>(naive_batch->schema()->field(0)->type());
+    EXPECT_TRUE(naive_type->timezone().empty());
+    const auto naive_array =
+            
std::static_pointer_cast<arrow::TimestampArray>(naive_batch->column(0));
+    // A timezone-naive Arrow timestamp preserves the 15:00:00.123456 
wall-clock value.
+    EXPECT_EQ(1783004400123456, naive_array->Value(0));
+}
+
 } // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
 
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
index b89abf16e83..a66d2e9648a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
@@ -33,7 +33,6 @@ import org.apache.doris.thrift.TGetTablesParams;
 import org.apache.doris.thrift.TListTableStatusResult;
 import org.apache.doris.thrift.TTableStatus;
 
-import org.apache.arrow.adapter.jdbc.JdbcToArrowUtils;
 import org.apache.arrow.flight.sql.FlightSqlColumnMetadata;
 import org.apache.arrow.flight.sql.impl.FlightSql.CommandGetDbSchemas;
 import org.apache.arrow.flight.sql.impl.FlightSql.CommandGetTables;
@@ -92,8 +91,7 @@ public class FlightSqlSchemaHelper {
      * Ref: `convert_to_arrow_type` in be/src/util/arrow/row_batch.cpp.
      * which is consistent with the type of Arrow data returned by Doris Arrow 
Flight Sql query.
      */
-    private static ArrowType getArrowType(PrimitiveType primitiveType, Integer 
precision, Integer scale,
-            String timeZone) {
+    private static ArrowType getArrowType(PrimitiveType primitiveType, Integer 
precision, Integer scale) {
         switch (primitiveType) {
             case BOOLEAN:
                 return new ArrowType.Bool();
@@ -128,11 +126,11 @@ public class FlightSqlSchemaHelper {
                 return new ArrowType.Date(DateUnit.DAY);
             case DATETIMEV2:
                 if (scale > 3) {
-                    return new ArrowType.Timestamp(TimeUnit.MICROSECOND, 
timeZone);
+                    return new ArrowType.Timestamp(TimeUnit.MICROSECOND, null);
                 } else if (scale > 0) {
-                    return new ArrowType.Timestamp(TimeUnit.MILLISECOND, 
timeZone);
+                    return new ArrowType.Timestamp(TimeUnit.MILLISECOND, null);
                 } else {
-                    return new ArrowType.Timestamp(TimeUnit.SECOND, timeZone);
+                    return new ArrowType.Timestamp(TimeUnit.SECOND, null);
                 }
             case TIMESTAMPTZ:
                 if (scale > 3) {
@@ -169,9 +167,7 @@ public class FlightSqlSchemaHelper {
         PrimitiveType primitiveType = 
PrimitiveType.fromThrift(desc.getColumnType());
         Integer precision = desc.isSetColumnPrecision() ? 
desc.getColumnPrecision() : null;
         Integer scale = desc.isSetColumnScale() ? desc.getColumnScale() : null;
-        // TODO there is no timezone in TColumnDesc, so use current timezone.
-        String timeZone = 
JdbcToArrowUtils.getUtcCalendar().getTimeZone().getID();
-        return getArrowType(primitiveType, precision, scale, timeZone);
+        return getArrowType(primitiveType, precision, scale);
     }
 
     private static Map<String, String> createFlightSqlColumnMetadata(final 
String dbName, final String tableName,
diff --git a/regression-test/data/arrow_flight_sql_p0/test_select.out 
b/regression-test/data/arrow_flight_sql_p0/test_select.out
index 62888cd3dfc..fb6bea38e2c 100644
--- a/regression-test/data/arrow_flight_sql_p0/test_select.out
+++ b/regression-test/data/arrow_flight_sql_p0/test_select.out
@@ -3,9 +3,9 @@
 777    4
 
 -- !arrow_flight_sql_datetime --
-333    plsql333        2024-07-21 12:00:00.123456      2024-07-21 12:00:00.0
-222    plsql222        2024-07-20 12:00:00.123456      2024-07-20 12:00:00.0
-111    plsql111        2024-07-19 12:00:00.123456      2024-07-19 12:00:00.0
+333    plsql333        2024-07-21 12:00:00.123456      2024-07-21 12:00:00
+222    plsql222        2024-07-20 12:00:00.123456      2024-07-20 12:00:00
+111    plsql111        2024-07-19 12:00:00.123456      2024-07-19 12:00:00
 
 -- !arrow_flight_sql_jsonb --
 1      {"k1":1,"k2":"v2"}
diff --git a/regression-test/suites/arrow_flight_sql_p0/test_select.groovy 
b/regression-test/suites/arrow_flight_sql_p0/test_select.groovy
index 85f119fc2c3..2a129f20abb 100644
--- a/regression-test/suites/arrow_flight_sql_p0/test_select.groovy
+++ b/regression-test/suites/arrow_flight_sql_p0/test_select.groovy
@@ -15,6 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
+import java.sql.Types
+import java.time.LocalDateTime
+
 suite("test_select", "arrow_flight_sql") {
     def tableName = "test_select"
     sql "DROP TABLE IF EXISTS ${tableName}"
@@ -32,14 +35,66 @@ suite("test_select", "arrow_flight_sql") {
     tableName = "test_select_datetime"
     sql "DROP TABLE IF EXISTS ${tableName}"
     sql """
-        create table ${tableName} (id int, name varchar(20), f_datetime_p 
datetime(6), f_datetime datetime) DUPLICATE key(`id`) distributed by hash 
(`id`) buckets 4
+        create table ${tableName} (id int, name varchar(20), f_datetime_p 
datetime(6),
+            f_datetime datetime, f_timestamptz timestamptz(6))
+        DUPLICATE key(`id`) distributed by hash (`id`) buckets 4
         properties ("replication_num"="1");
         """
-    sql """INSERT INTO ${tableName} VALUES(111, "plsql111","2024-07-19 
12:00:00.123456","2024-07-19 12:00:00")"""
-    sql """INSERT INTO ${tableName} VALUES(222, "plsql222","2024-07-20 
12:00:00.123456","2024-07-20 12:00:00")"""
-    sql """INSERT INTO ${tableName} VALUES(333, "plsql333","2024-07-21 
12:00:00.123456","2024-07-21 12:00:00")"""
+    sql """INSERT INTO ${tableName} VALUES
+        (111, "plsql111", "2024-07-19 12:00:00.123456", "2024-07-19 12:00:00",
+            "2024-07-19 12:00:00.123456 +08:00"),
+        (222, "plsql222", "2024-07-20 12:00:00.123456", "2024-07-20 12:00:00",
+            "2024-07-20 12:00:00.123456 +08:00"),
+        (333, "plsql333", "2024-07-21 12:00:00.123456", "2024-07-21 12:00:00",
+            "2024-07-21 12:00:00.123456 +08:00")
+    """
+
+    // Arrow JDBC's untyped getObject() applies timezone conversion to a naive 
timestamp. Keep the
+    // snapshot comparison textual and verify the timestamp schema and 
wall-clock values explicitly.
+    qt_arrow_flight_sql_datetime """
+        SELECT id, name, CAST(f_datetime_p AS STRING), CAST(f_datetime AS 
STRING)
+        FROM ${tableName}
+        ORDER BY id DESC
+    """
+
+    def discoveredColumnTypes = [:]
+    context.getArrowFlightSqlConnection().getMetaData()
+            .getColumns(null, context.dbName, tableName, null).withCloseable { 
columns ->
+        while (columns.next()) {
+            discoveredColumnTypes[columns.getString("COLUMN_NAME")] = 
columns.getInt("DATA_TYPE")
+        }
+    }
+    assertEquals(Types.TIMESTAMP, discoveredColumnTypes["f_datetime_p"])
+    assertEquals(Types.TIMESTAMP, discoveredColumnTypes["f_datetime"])
+    assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, 
discoveredColumnTypes["f_timestamptz"])
+
+    context.getArrowFlightSqlConnection().createStatement().withCloseable { 
statement ->
+        statement.executeQuery("""
+            USE ${context.dbName};
+            SELECT
+                CAST('2024-07-19 12:00:00' AS DATETIME(0)) AS datetime_s,
+                CAST('2024-07-19 12:00:00.123' AS DATETIME(3)) AS datetime_ms,
+                CAST('2024-07-19 12:00:00.123456' AS DATETIME(6)) AS 
datetime_us,
+                CAST('2024-07-19 12:00:00.123456 +08:00' AS TIMESTAMPTZ(6)) AS 
timestamptz_us
+        """).withCloseable { resultSet ->
+            def metadata = resultSet.getMetaData()
+            assertEquals(4, metadata.getColumnCount())
+            for (int i = 1; i <= 3; i++) {
+                assertEquals(Types.TIMESTAMP, metadata.getColumnType(i))
+                assertEquals("TIMESTAMP", metadata.getColumnTypeName(i))
+            }
+            assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, 
metadata.getColumnType(4))
+            assertEquals("TIMESTAMP_WITH_TIMEZONE", 
metadata.getColumnTypeName(4))
 
-    qt_arrow_flight_sql_datetime "select * from ${tableName} order by id desc"
+            assertTrue(resultSet.next())
+            assertEquals(LocalDateTime.parse("2024-07-19T12:00:00"),
+                    resultSet.getObject(1, LocalDateTime.class))
+            assertEquals(LocalDateTime.parse("2024-07-19T12:00:00.123"),
+                    resultSet.getObject(2, LocalDateTime.class))
+            assertEquals(LocalDateTime.parse("2024-07-19T12:00:00.123456"),
+                    resultSet.getObject(3, LocalDateTime.class))
+        }
+    }
 
     tableName = "test_select_jsonb"
     sql "DROP TABLE IF EXISTS ${tableName}"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to