Skip to content

fetch_df_all returns the wrong instant for TIMESTAMP WITH TIME ZONE in Thick mode (zone offset added to the wall-clock time) #596

Description

@goldmedal
  1. What versions are you using?
platform.platform: macOS-26.5.1-arm64-arm-64bit  (also reproduced on Linux x86_64)
sys.maxsize > 2**32: True
platform.python_version: 3.11.9
oracledb.__version__: 3.3.0
oracledb.clientversion(): (23, 3, 0, 23, 9)   # Oracle Instant Client 23.3
connection.version: 21.3.0.0.0                 # gvenzl/oracle-xe:21 (also reproduced on 23.x XE)
pyarrow: 22.0.0
  1. Is it an error or a hang or a crash?

Neither — it silently returns the wrong value. No exception is raised.

  1. What error(s) or behavior are you seeing?

For an offset-based TIMESTAMP WITH TIME ZONE value, connection.fetch_df_all() (the Arrow dataframe path) returns an instant equal to wall_clock_time + zone_offset, instead of normalizing to UTC. The regular cursor.fetchall() / fetchone() path is correct.

For TIMESTAMP '2026-06-01 19:00:00 +08:00':

Fetch path Result Comment
cursor.fetchone() 2026-06-01 19:00:00 wall-clock time (tz-naive — see #274)
fetch_df_all() Thick 2026-06-02 03:00:00 19:00 + 08:00
fetch_df_all() Thin 2026-06-01 19:00:00 wall-clock time (offset dropped)

The offset is applied per value, in the wrong direction, which is what makes the Thick-mode result neither UTC nor wall-clock:

Literal fetch_df_all (Thick) Expected UTC instant
19:00:00 +08:00 2026-06-02 03:00:00 (+8h) 2026-06-01 11:00:00
19:00:00 -05:00 2026-06-01 14:00:00 (-5h) 2026-06-02 00:00:00

So the same value comes back differently depending on its offset, confirming the offset is being added to the wall-clock time rather than subtracted to obtain UTC.

Notes:

  • This looks related to Fetching DATE/TIMESTAMP as DF converts to UTC TZ #499 (DATE/TIMESTAMP DF→UTC conversion, fixed in 3.2.0), but that fix did not cover TIMESTAMP WITH TIME ZONE: the bug is still present on 3.3.0.
  • Thin mode does not reproduce — it returns the wall-clock time (offset dropped). Only Thick mode double-applies the offset, so this appears to be in the Thick-mode Arrow conversion for DB_TYPE_TIMESTAMP_TZ / DB_TYPE_TIMESTAMP_LTZ.
  • TIMESTAMP WITH LOCAL TIME ZONE is affected the same way.
  1. Does your application call init_oracle_client()?

Yes — the bug reproduces in Thick mode. In Thin mode it does not (see table above).

  1. Include a runnable Python script that shows the problem.

No table is required — the script uses dual and TIMESTAMP literals:

import oracledb
import pyarrow as pa

# Comment out init_oracle_client() to run in Thin mode, where the bug does NOT
# reproduce (Thin returns the wall-clock time instead).
oracledb.init_oracle_client()  # Thick mode

conn = oracledb.connect(user="...", password="...", dsn="host:port/SERVICE")
sql = (
    "SELECT TIMESTAMP '2026-06-01 19:00:00 +08:00' AS t_plus8, "
    "TIMESTAMP '2026-06-01 19:00:00 -05:00' AS t_minus5 FROM dual"
)

with conn.cursor() as cur:
    cur.execute(sql)
    print("fetchone     ->", cur.fetchone())
    # Thick: (datetime(2026, 6, 1, 19, 0), datetime(2026, 6, 1, 19, 0))

odf = conn.fetch_df_all(sql)
tbl = pa.Table.from_arrays(odf.column_arrays(), names=odf.column_names())
print("fetch_df_all ->", tbl.to_pylist())
# Thick: [{'T_PLUS8': 2026-06-02 03:00:00, 'T_MINUS5': 2026-06-01 14:00:00}]  <-- wall + offset
# Thin : [{'T_PLUS8': 2026-06-01 19:00:00, 'T_MINUS5': 2026-06-01 19:00:00}]  <-- wall-clock
# Expected UTC: 2026-06-01 11:00:00 and 2026-06-02 00:00:00

Expected: fetch_df_all should return the same instant the regular fetch represents (ideally a tz-aware / UTC-normalized Arrow timestamp), not wall_clock + offset.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions