88from  narwhals .utils  import  Version , isinstance_or_issubclass 
99
1010if  TYPE_CHECKING :
11-     from  duckdb  import  Expression 
11+     from  duckdb  import  DuckDBPyRelation ,  Expression 
1212    from  duckdb .typing  import  DuckDBPyType 
1313
1414    from  narwhals ._duckdb .dataframe  import  DuckDBLazyFrame 
@@ -95,21 +95,56 @@ def evaluate_exprs(
9595    return  native_results 
9696
9797
98- def  native_to_narwhals_dtype (duckdb_dtype : DuckDBPyType , version : Version ) ->  DType :
98+ class  DeferredTimeZone :
99+     """Object which gets passed between `native_to_narwhals_dtype` calls. 
100+ 
101+     DuckDB stores the time zone in the connection, rather than in the dtypes, so 
102+     this ensures that when calculating the schema of a dataframe with multiple 
103+     timezone-aware columns, that the connection's time zone is only fetched once. 
104+ 
105+     Note: we cannot make the time zone a cached `DuckDBLazyFrame` property because 
106+     the time zone can be modified after `DuckDBLazyFrame` creation: 
107+ 
108+     ```python 
109+     df = nw.from_native(rel) 
110+     print(df.collect_schema()) 
111+     rel.query("set timezone = 'Asia/Kolkata'") 
112+     print(df.collect_schema())  # should change to reflect new time zone 
113+     ``` 
114+     """ 
115+ 
116+     _cached_time_zone : str  |  None  =  None 
117+ 
118+     def  __init__ (self , rel : DuckDBPyRelation ) ->  None :
119+         self ._rel  =  rel 
120+ 
121+     @property  
122+     def  time_zone (self ) ->  str :
123+         """Fetch relation time zone (if it wasn't calculated already).""" 
124+         if  self ._cached_time_zone  is  None :
125+             self ._cached_time_zone  =  fetch_rel_time_zone (self ._rel )
126+         return  self ._cached_time_zone 
127+ 
128+ 
129+ def  native_to_narwhals_dtype (
130+     duckdb_dtype : DuckDBPyType , version : Version , deferred_time_zone : DeferredTimeZone 
131+ ) ->  DType :
99132    duckdb_dtype_id  =  duckdb_dtype .id 
100133    dtypes  =  version .dtypes 
101134
102135    # Handle nested data types first 
103136    if  duckdb_dtype_id  ==  "list" :
104-         return  dtypes .List (native_to_narwhals_dtype (duckdb_dtype .child , version = version ))
137+         return  dtypes .List (
138+             native_to_narwhals_dtype (duckdb_dtype .child , version , deferred_time_zone )
139+         )
105140
106141    if  duckdb_dtype_id  ==  "struct" :
107142        children  =  duckdb_dtype .children 
108143        return  dtypes .Struct (
109144            [
110145                dtypes .Field (
111146                    name = child [0 ],
112-                     dtype = native_to_narwhals_dtype (child [1 ], version = version ),
147+                     dtype = native_to_narwhals_dtype (child [1 ], version ,  deferred_time_zone ),
113148                )
114149                for  child  in  children 
115150            ]
@@ -123,7 +158,7 @@ def native_to_narwhals_dtype(duckdb_dtype: DuckDBPyType, version: Version) -> DT
123158            child , size  =  child [1 ].children 
124159            shape .insert (0 , size [1 ])
125160
126-         inner  =  native_to_narwhals_dtype (child [1 ], version = version )
161+         inner  =  native_to_narwhals_dtype (child [1 ], version ,  deferred_time_zone )
127162        return  dtypes .Array (inner = inner , shape = tuple (shape ))
128163
129164    if  duckdb_dtype_id  ==  "enum" :
@@ -132,9 +167,20 @@ def native_to_narwhals_dtype(duckdb_dtype: DuckDBPyType, version: Version) -> DT
132167        categories  =  duckdb_dtype .children [0 ][1 ]
133168        return  dtypes .Enum (categories = categories )
134169
170+     if  duckdb_dtype_id  ==  "timestamp with time zone" :
171+         return  dtypes .Datetime (time_zone = deferred_time_zone .time_zone )
172+ 
135173    return  _non_nested_native_to_narwhals_dtype (duckdb_dtype_id , version )
136174
137175
176+ def  fetch_rel_time_zone (rel : duckdb .DuckDBPyRelation ) ->  str :
177+     result  =  rel .query (
178+         "duckdb_settings()" , "select value from duckdb_settings() where name = 'TimeZone'" 
179+     ).fetchone ()
180+     assert  result  is  not   None   # noqa: S101 
181+     return  result [0 ]
182+ 
183+ 
138184@lru_cache (maxsize = 16 ) 
139185def  _non_nested_native_to_narwhals_dtype (duckdb_dtype_id : str , version : Version ) ->  DType :
140186    dtypes  =  version .dtypes 
@@ -154,9 +200,6 @@ def _non_nested_native_to_narwhals_dtype(duckdb_dtype_id: str, version: Version)
154200        "varchar" : dtypes .String (),
155201        "date" : dtypes .Date (),
156202        "timestamp" : dtypes .Datetime (),
157-         # TODO(marco): is UTC correct, or should we be getting the connection timezone? 
158-         # https://github.com/narwhals-dev/narwhals/issues/2165 
159-         "timestamp with time zone" : dtypes .Datetime (time_zone = "UTC" ),
160203        "boolean" : dtypes .Boolean (),
161204        "interval" : dtypes .Duration (),
162205        "decimal" : dtypes .Decimal (),
0 commit comments