Skip to content

Commit d6a4749

Browse files
authored
SNOW-770678: Fix incorrect date conversion in bulk insertion (#1562)
1 parent bde8101 commit d6a4749

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1717
- Fixed a bug when `_prefetch_hook()` was not called before yielding results of `execute_async()`.
1818
- Fixed a bug where some ResultMetadata fields were marked as required when they were optional.
1919
- Bumped pandas dependency from <1.6.0,>=1.0.0 to >=1.0.0,<2.1.0
20+
- Fixed a bug where bulk insert converts date incorrectly.
2021

2122
- v3.0.3(April 20, 2023)
2223
- Fixed a bug that prints error in logs for GET command on GCS.

src/snowflake/connector/connection.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
OCSPMode,
5858
QueryStatus,
5959
)
60-
from .converter import SnowflakeConverter
60+
from .converter import SnowflakeConverter, _adjust_bind_type
6161
from .cursor import LOG_MAX_QUERY_LENGTH, SnowflakeCursor
6262
from .description import (
6363
CLIENT_NAME,
@@ -1261,13 +1261,13 @@ def _process_params_qmarks(
12611261
if all(param_data.type == first_type for param_data in all_param_data):
12621262
snowflake_type = first_type
12631263
processed_params[str(idx + 1)] = {
1264-
"type": snowflake_type,
1264+
"type": _adjust_bind_type(snowflake_type),
12651265
"value": [param_data.binding for param_data in all_param_data],
12661266
}
12671267
else:
12681268
snowflake_type, snowflake_binding = get_type_and_binding(v)
12691269
processed_params[str(idx + 1)] = {
1270-
"type": snowflake_type,
1270+
"type": _adjust_bind_type(snowflake_type),
12711271
"value": snowflake_binding,
12721272
}
12731273
if logger.getEffectiveLevel() <= logging.DEBUG:

src/snowflake/connector/converter.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ def _convert_datetime_to_epoch_nanoseconds(dt: datetime) -> str:
101101
return f"{convert_datetime_to_epoch(dt):f}".replace(".", "") + "000"
102102

103103

104-
def _convert_date_to_epoch_milliseconds(dt: datetime) -> str:
105-
return f"{(dt - ZERO_EPOCH_DATE).total_seconds():.3f}".replace(".", "")
106-
107-
108104
def _convert_time_to_epoch_nanoseconds(tm: dt_t) -> str:
109105
return (
110106
str(tm.hour * 3600 + tm.minute * 60 + tm.second)
@@ -366,8 +362,8 @@ def _nonetype_to_snowflake_bindings(self, *_) -> None:
366362
return None
367363

368364
def _date_to_snowflake_bindings(self, _, value: date) -> str:
369-
# milliseconds
370-
return _convert_date_to_epoch_milliseconds(value)
365+
# we are binding "TEXT" value for DATE, check function _adjust_bind_type
366+
return value.isoformat()
371367

372368
def _time_to_snowflake_bindings(self, _, value: dt_t) -> str:
373369
# nanoseconds
@@ -735,3 +731,11 @@ def create_timestamp_from_string(
735731
if not tz:
736732
return datetime.utcfromtimestamp(seconds) + timedelta(microseconds=fraction)
737733
return datetime.fromtimestamp(seconds, tz=tz) + timedelta(microseconds=fraction)
734+
735+
736+
def _adjust_bind_type(input_type: str | None) -> str | None:
737+
# This is to address SNOW-7706788, binding "DATE" value can not go beyond date 2969-05-03 (31536000000)
738+
# https://docs.snowflake.com/en/sql-reference/functions/to_date#usage-notes
739+
# https://docs.snowflake.com/en/developer-guide/sql-api/submitting-requests
740+
# to correctly bind DATE value, we adjust to use "TEXT" type to bind value
741+
return input_type if input_type != "DATE" else "TEXT"

test/integ/test_bindings.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,55 @@ def test_binding_bulk_insert(conn_cnx, db_parameters, num_rows):
431431
)
432432

433433

434+
@pytest.mark.skipolddriver
435+
def test_binding_bulk_insert_date(conn_cnx, db_parameters):
436+
"""Bulk insert test."""
437+
with conn_cnx() as cnx:
438+
cnx.cursor().execute(
439+
"""
440+
create or replace table {name} (
441+
c1 date
442+
)
443+
""".format(
444+
name=db_parameters["name"]
445+
)
446+
)
447+
try:
448+
with conn_cnx(paramstyle="qmark") as cnx:
449+
c = cnx.cursor()
450+
cnx._session_parameters[CLIENT_STAGE_ARRAY_BINDING_THRESHOLD] = 1
451+
dates = [
452+
[date.fromisoformat("1750-05-09")],
453+
[date.fromisoformat("1969-12-31")],
454+
[date.fromisoformat("1970-01-01")],
455+
[date.fromisoformat("2023-05-12")],
456+
[date.fromisoformat("2999-12-31")],
457+
[date.fromisoformat("3000-12-31")],
458+
[date.fromisoformat("9999-12-31")],
459+
]
460+
c.executemany(f'INSERT INTO {db_parameters["name"]}(c1) VALUES (?)', dates)
461+
assert c.rowcount == len(dates)
462+
ret = c.execute(f'SELECT c1 from {db_parameters["name"]}').fetchall()
463+
assert ret == [
464+
(date(1750, 5, 9),),
465+
(date(1969, 12, 31),),
466+
(date(1970, 1, 1),),
467+
(date(2023, 5, 12),),
468+
(date(2999, 12, 31),),
469+
(date(3000, 12, 31),),
470+
(date(9999, 12, 31),),
471+
]
472+
finally:
473+
with conn_cnx() as cnx:
474+
cnx.cursor().execute(
475+
"""
476+
drop table if exists {name}
477+
""".format(
478+
name=db_parameters["name"]
479+
)
480+
)
481+
482+
434483
@pytest.mark.skipolddriver
435484
def test_bulk_insert_binding_fallback(conn_cnx):
436485
"""When stage creation fails, bulk inserts falls back to server side binding and disables stage optimization."""

0 commit comments

Comments
 (0)