Skip to content

Commit d296b0d

Browse files
authored
SNOW-966444: Fix date binding with qmark (#1819)
1 parent 47b7e20 commit d296b0d

File tree

4 files changed

+53
-17
lines changed

4 files changed

+53
-17
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1515
- Changed urllib3 version pin to only affect Python versions < 3.10.
1616
- Support for `private_key_file` and `private_key_file_pwd` connection parameters
1717
- Added a new flag `expired` to `SnowflakeConnection` class, that keeps track of whether the connection's master token has expired.
18+
- Fixed a bug where date insertion failed when date format is set and qmark style binding is used.
1819

1920
- v3.5.0(November 13,2023)
2021

src/snowflake/connector/connection.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
OCSPMode,
6868
QueryStatus,
6969
)
70-
from .converter import SnowflakeConverter, _adjust_bind_type
70+
from .converter import SnowflakeConverter
7171
from .cursor import LOG_MAX_QUERY_LENGTH, SnowflakeCursor
7272
from .description import (
7373
CLIENT_NAME,
@@ -1433,13 +1433,13 @@ def _process_params_qmarks(
14331433
if all(param_data.type == first_type for param_data in all_param_data):
14341434
snowflake_type = first_type
14351435
processed_params[str(idx + 1)] = {
1436-
"type": _adjust_bind_type(snowflake_type),
1436+
"type": snowflake_type,
14371437
"value": [param_data.binding for param_data in all_param_data],
14381438
}
14391439
else:
14401440
snowflake_type, snowflake_binding = get_type_and_binding(v)
14411441
processed_params[str(idx + 1)] = {
1442-
"type": _adjust_bind_type(snowflake_type),
1442+
"type": snowflake_type,
14431443
"value": snowflake_binding,
14441444
}
14451445
if logger.getEffectiveLevel() <= logging.DEBUG:

src/snowflake/connector/converter.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ def _convert_time_to_epoch_nanoseconds(tm: dt_t) -> str:
110110
)
111111

112112

113+
def _convert_date_to_epoch_seconds(dt: date) -> str:
114+
return f"{int((dt - ZERO_EPOCH_DATE).total_seconds())}"
115+
116+
117+
def _convert_date_to_epoch_milliseconds(dt: date) -> str:
118+
return f"{(dt - ZERO_EPOCH_DATE).total_seconds():.3f}".replace(".", "")
119+
120+
121+
def _convert_date_to_epoch_nanoseconds(dt: date) -> str:
122+
return f"{(dt - ZERO_EPOCH_DATE).total_seconds():.9f}".replace(".", "")
123+
124+
113125
def _extract_timestamp(value: str, ctx: dict) -> tuple[float, int]:
114126
"""Extracts timestamp from a raw data."""
115127
scale = ctx["scale"]
@@ -366,8 +378,15 @@ def _nonetype_to_snowflake_bindings(self, *_) -> None:
366378
return None
367379

368380
def _date_to_snowflake_bindings(self, _, value: date) -> str:
369-
# we are binding "TEXT" value for DATE, check function _adjust_bind_type
370-
return value.isoformat()
381+
milliseconds = _convert_date_to_epoch_milliseconds(value)
382+
# according to https://docs.snowflake.com/en/sql-reference/functions/to_date
383+
# through test, value in seconds will lead to wrong date
384+
# millisecond and nanoarrow second are good
385+
# if the milliseconds is beyond the range of 31536000000000, we switch to use nanoseconds
386+
# otherwise we will hit overflow error in snowflake
387+
if int(milliseconds) < 31536000000000:
388+
return milliseconds
389+
return _convert_date_to_epoch_nanoseconds(value)
371390

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

test/integ/test_bindings.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,6 @@ def test_binding_bulk_insert_date(conn_cnx, db_parameters):
449449
c = cnx.cursor()
450450
cnx._session_parameters[CLIENT_STAGE_ARRAY_BINDING_THRESHOLD] = 1
451451
dates = [
452-
[date.fromisoformat("1750-05-09")],
453-
[date.fromisoformat("1969-12-31")],
454452
[date.fromisoformat("1970-01-01")],
455453
[date.fromisoformat("2023-05-12")],
456454
[date.fromisoformat("2999-12-31")],
@@ -461,14 +459,27 @@ def test_binding_bulk_insert_date(conn_cnx, db_parameters):
461459
assert c.rowcount == len(dates)
462460
ret = c.execute(f'SELECT c1 from {db_parameters["name"]}').fetchall()
463461
assert ret == [
464-
(date(1750, 5, 9),),
465-
(date(1969, 12, 31),),
466462
(date(1970, 1, 1),),
467463
(date(2023, 5, 12),),
468464
(date(2999, 12, 31),),
469465
(date(3000, 12, 31),),
470466
(date(9999, 12, 31),),
471467
]
468+
cnx.cursor().execute(f"TRUNCATE TABLE {db_parameters['name']}")
469+
# TODO: bulk insert mixing date < 1970-01-01 and >= 1970-01-01 can return wrong result
470+
# here we split the insertion to make sure the client logic is correct
471+
# this could be a bug in snowflake
472+
dates = [
473+
[date.fromisoformat("1750-05-09")],
474+
[date.fromisoformat("1969-12-31")],
475+
]
476+
c.executemany(f'INSERT INTO {db_parameters["name"]}(c1) VALUES (?)', dates)
477+
assert c.rowcount == len(dates)
478+
ret = c.execute(f'SELECT c1 from {db_parameters["name"]}').fetchall()
479+
assert ret == [
480+
(date(1750, 5, 9),),
481+
(date(1969, 12, 31),),
482+
]
472483
finally:
473484
with conn_cnx() as cnx:
474485
cnx.cursor().execute(
@@ -480,6 +491,19 @@ def test_binding_bulk_insert_date(conn_cnx, db_parameters):
480491
)
481492

482493

494+
@pytest.mark.skipolddriver
495+
def test_binding_insert_date(conn_cnx, db_parameters):
496+
bind_query = "SELECT TRY_TO_DATE(TO_CHAR(?,?),?)"
497+
bind_variables = (date(2016, 4, 10), "YYYY-MM-DD", "YYYY-MM-DD")
498+
bind_variables_2 = (date(2016, 4, 10), "YYYY-MM-DD", "DD-MON-YYYY")
499+
with conn_cnx(paramstyle="qmark") as cnx, cnx.cursor() as cursor:
500+
assert cursor.execute(bind_query, bind_variables).fetchall() == [
501+
(date(2016, 4, 10),)
502+
]
503+
# the second sql returns None because 2016-04-10 doesn't comply with the format DD-MON-YYYY
504+
assert cursor.execute(bind_query, bind_variables_2).fetchall() == [(None,)]
505+
506+
483507
@pytest.mark.skipolddriver
484508
def test_bulk_insert_binding_fallback(conn_cnx):
485509
"""When stage creation fails, bulk inserts falls back to server side binding and disables stage optimization."""

0 commit comments

Comments
 (0)