@@ -118,12 +118,15 @@ def _create_temp_stage(
118
118
119
119
120
120
def _do_create_temp_file_format (
121
- cursor : SnowflakeCursor , file_format_location : str , compression : str
121
+ cursor : SnowflakeCursor ,
122
+ file_format_location : str ,
123
+ compression : str ,
124
+ sql_use_logical_type : str ,
122
125
) -> None :
123
126
file_format_sql = (
124
127
f"CREATE TEMP FILE FORMAT { file_format_location } "
125
128
f"/* Python:snowflake.connector.pandas_tools.write_pandas() */ "
126
- f"TYPE=PARQUET COMPRESSION={ compression } "
129
+ f"TYPE=PARQUET COMPRESSION={ compression } { sql_use_logical_type } "
127
130
)
128
131
logger .debug (f"creating file format with '{ file_format_sql } '" )
129
132
cursor .execute (file_format_sql , _is_internal = True )
@@ -135,6 +138,7 @@ def _create_temp_file_format(
135
138
schema : str | None ,
136
139
quote_identifiers : bool ,
137
140
compression : str ,
141
+ sql_use_logical_type : str ,
138
142
) -> str :
139
143
file_format_name = random_string ()
140
144
file_format_location = build_location_helper (
@@ -144,15 +148,19 @@ def _create_temp_file_format(
144
148
quote_identifiers = quote_identifiers ,
145
149
)
146
150
try :
147
- _do_create_temp_file_format (cursor , file_format_location , compression )
151
+ _do_create_temp_file_format (
152
+ cursor , file_format_location , compression , sql_use_logical_type
153
+ )
148
154
except ProgrammingError as e :
149
155
# User may not have the privilege to create file format on the target schema, so fall back to use current schema
150
156
# as the old behavior.
151
157
logger .debug (
152
158
f"creating stage { file_format_location } failed. Exception { str (e )} . Fall back to use current schema"
153
159
)
154
160
file_format_location = file_format_name
155
- _do_create_temp_file_format (cursor , file_format_location , compression )
161
+ _do_create_temp_file_format (
162
+ cursor , file_format_location , compression , sql_use_logical_type
163
+ )
156
164
157
165
return file_format_location
158
166
@@ -172,6 +180,7 @@ def write_pandas(
172
180
create_temp_table : bool = False ,
173
181
overwrite : bool = False ,
174
182
table_type : Literal ["" , "temp" , "temporary" , "transient" ] = "" ,
183
+ use_logical_type : bool | None = None ,
175
184
** kwargs : Any ,
176
185
) -> tuple [
177
186
bool ,
@@ -232,6 +241,11 @@ def write_pandas(
232
241
Pandas DataFrame.
233
242
table_type: The table type of to-be-created table. The supported table types include ``temp``/``temporary``
234
243
and ``transient``. Empty means permanent table as per SQL convention.
244
+ use_logical_type: Boolean that specifies whether to use Parquet logical types. With this file format option,
245
+ Snowflake can interpret Parquet logical types during data loading. To enable Parquet logical types,
246
+ set use_logical_type as True. Set to None to use Snowflakes default. For more information, see:
247
+ https://docs.snowflake.com/en/sql-reference/sql/create-file-format
248
+
235
249
236
250
Returns:
237
251
Returns the COPY INTO command's results to verify ingestion in the form of a tuple of whether all chunks were
@@ -280,6 +294,27 @@ def write_pandas(
280
294
stacklevel = 2 ,
281
295
)
282
296
297
+ # use_logical_type should be True when dataframe contains datetimes with timezone.
298
+ # https://github.com/snowflakedb/snowflake-connector-python/issues/1687
299
+ if not use_logical_type and any (
300
+ [pandas .api .types .is_datetime64tz_dtype (df [c ]) for c in df .columns ]
301
+ ):
302
+ warnings .warn (
303
+ "Dataframe contains a datetime with timezone column, but "
304
+ f"'{ use_logical_type = } '. This can result in dateimes "
305
+ "being incorrectly written to Snowflake. Consider setting "
306
+ "'use_logical_type = True'" ,
307
+ UserWarning ,
308
+ stacklevel = 2 ,
309
+ )
310
+
311
+ if use_logical_type is None :
312
+ sql_use_logical_type = ""
313
+ elif use_logical_type :
314
+ sql_use_logical_type = " USE_LOGICAL_TYPE = TRUE"
315
+ else :
316
+ sql_use_logical_type = " USE_LOGICAL_TYPE = FALSE"
317
+
283
318
cursor = conn .cursor ()
284
319
stage_location = _create_temp_stage (
285
320
cursor ,
@@ -329,7 +364,12 @@ def drop_object(name: str, object_type: str) -> None:
329
364
330
365
if auto_create_table or overwrite :
331
366
file_format_location = _create_temp_file_format (
332
- cursor , database , schema , quote_identifiers , compression_map [compression ]
367
+ cursor ,
368
+ database ,
369
+ schema ,
370
+ quote_identifiers ,
371
+ compression_map [compression ],
372
+ sql_use_logical_type ,
333
373
)
334
374
infer_schema_sql = f"SELECT COLUMN_NAME, TYPE FROM table(infer_schema(location=>'@{ stage_location } ', file_format=>'{ file_format_location } '))"
335
375
logger .debug (f"inferring schema with '{ infer_schema_sql } '" )
@@ -381,7 +421,12 @@ def drop_object(name: str, object_type: str) -> None:
381
421
f"COPY INTO { target_table_location } /* Python:snowflake.connector.pandas_tools.write_pandas() */ "
382
422
f"({ columns } ) "
383
423
f"FROM (SELECT { parquet_columns } FROM @{ stage_location } ) "
384
- f"FILE_FORMAT=(TYPE=PARQUET COMPRESSION={ compression_map [compression ]} { ' BINARY_AS_TEXT=FALSE' if auto_create_table or overwrite else '' } ) "
424
+ f"FILE_FORMAT=("
425
+ f"TYPE=PARQUET "
426
+ f"COMPRESSION={ compression_map [compression ]} "
427
+ f"{ ' BINARY_AS_TEXT=FALSE' if auto_create_table or overwrite else '' } "
428
+ f"{ sql_use_logical_type } "
429
+ f") "
385
430
f"PURGE=TRUE ON_ERROR={ on_error } "
386
431
)
387
432
logger .debug (f"copying into with '{ copy_into_sql } '" )
0 commit comments