Skip to content

Commit 7792c57

Browse files
Snow-2091473: converter doesnt properly convert lists of bytes (#2734)
Co-authored-by: Jeff Sawatzky <jeffsawatzky@users.noreply.github.com>
1 parent 1821e6d commit 7792c57

5 files changed

Lines changed: 207 additions & 1 deletion

File tree

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
88

99
# Release Notes
1010
- v4.2.1(TBD)
11+
- Ensured proper list conversion - the converter runs to_snowflake on all list elements.
1112
- Made the parameter `server_session_keep_alive` in `SnowflakeConnection` skip checking for pending async queries, providing faster connection close times especially when many async queries are executed.
1213
- Fix string representation of INTERVAL YEAR and INTERVAL MONTH types.
1314

src/snowflake/connector/converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ def _decimal_to_snowflake(self, value: decimal.Decimal) -> str | None:
648648
def _list_to_snowflake(self, value: list) -> list:
649649
return [
650650
SnowflakeConverter.quote(v0)
651-
for v0 in [SnowflakeConverter.escape(v) for v in value]
651+
for v0 in [SnowflakeConverter.escape(self.to_snowflake(v)) for v in value]
652652
]
653653

654654
_tuple_to_snowflake = _list_to_snowflake

test/integ/aio_it/test_converter_async.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,72 @@ async def test_franction_followed_by_year_format(conn_cnx):
468468
assert rec[0] == "05:34:56.123456 Jan 03, 2012"
469469

470470

471+
async def test_binary_list_parameter_in_query(conn_cnx):
472+
"""Test that binary data in list parameters works correctly.
473+
474+
Regression test for GitHub Issue #2311.
475+
When binary data (bytes) is passed in a list parameter for an IN clause,
476+
the converter should properly hex-encode each item in the list.
477+
Without the fix, this raises UnicodeDecodeError when bytes contain non-ASCII values.
478+
"""
479+
import uuid
480+
481+
# Use UUIDs that generate non-ASCII bytes (bytes > 127)
482+
business_uuid = uuid.UUID("4363f57d-c9ca-4e63-92c7-3e67786c8a30")
483+
user_uuid1 = uuid.UUID("4363f57d-c9ca-4e63-92c7-3e67786c8a30")
484+
user_uuid2 = uuid.UUID("f5abcdef-1234-5678-9abc-def012345678")
485+
486+
async with conn_cnx() as conn:
487+
cur = conn.cursor()
488+
try:
489+
# Create a temporary table for testing
490+
await cur.execute(
491+
"CREATE OR REPLACE TEMPORARY TABLE test_binary_list_param "
492+
"(business_uuid BINARY, user_uuid BINARY)"
493+
)
494+
495+
# Insert test data
496+
await cur.execute(
497+
"INSERT INTO test_binary_list_param VALUES (%(business_uuid)s, %(user_uuid)s)",
498+
params={
499+
"business_uuid": business_uuid.bytes,
500+
"user_uuid": user_uuid1.bytes,
501+
},
502+
)
503+
504+
# Test single binary parameter (this should work)
505+
await cur.execute(
506+
"SELECT * FROM test_binary_list_param WHERE business_uuid = %(business_uuid)s",
507+
params={"business_uuid": business_uuid.bytes},
508+
)
509+
results = await cur.fetchall()
510+
assert len(results) == 1
511+
512+
# Test binary in list parameter (this was failing with UnicodeDecodeError)
513+
await cur.execute(
514+
"SELECT * FROM test_binary_list_param "
515+
"WHERE business_uuid = %(business_uuid)s AND user_uuid IN (%(user_uuids)s)",
516+
params={
517+
"business_uuid": business_uuid.bytes,
518+
"user_uuids": [user_uuid1.bytes],
519+
},
520+
)
521+
results = await cur.fetchall()
522+
assert len(results) == 1
523+
524+
# Test with multiple binary values in list
525+
await cur.execute(
526+
"SELECT * FROM test_binary_list_param "
527+
"WHERE user_uuid IN (%(user_uuids)s)",
528+
params={"user_uuids": [user_uuid1.bytes, user_uuid2.bytes]},
529+
)
530+
results = await cur.fetchall()
531+
assert len(results) == 1 # Only user_uuid1 exists in the table
532+
533+
finally:
534+
await cur.execute("DROP TABLE IF EXISTS test_binary_list_param")
535+
536+
471537
async def test_fetch_fraction_timestamp(conn_cnx):
472538
"""Additional fetch timestamp tests. Mainly used for SnowSQL which converts to string representations."""
473539
PST_TZ = "America/Los_Angeles"

test/integ/test_converter.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,66 @@ def test_fetch_fraction_timestamp(conn_cnx):
535535
assert ret[9] == "2100-01-01 05:00:00.012000000"
536536
assert ret[10] == "1970-01-01 00:00:00.000000000 +0000"
537537
assert ret[11] == "1970-01-01 00:00:00.000000000"
538+
539+
540+
def test_binary_list_parameter_in_query(conn_cnx):
541+
"""Test that binary data in list parameters works correctly.
542+
543+
Regression test for GitHub Issue #2311.
544+
When binary data (bytes) is passed in a list parameter for an IN clause,
545+
the converter should properly hex-encode each item in the list.
546+
Without the fix, this raises UnicodeDecodeError when bytes contain non-ASCII values.
547+
"""
548+
import uuid
549+
550+
# Use UUIDs that generate non-ASCII bytes (bytes > 127)
551+
business_uuid = uuid.UUID("4363f57d-c9ca-4e63-92c7-3e67786c8a30")
552+
user_uuid1 = uuid.UUID("4363f57d-c9ca-4e63-92c7-3e67786c8a30")
553+
user_uuid2 = uuid.UUID("f5abcdef-1234-5678-9abc-def012345678")
554+
555+
with conn_cnx() as conn:
556+
cur = conn.cursor()
557+
try:
558+
# Create a temporary table for testing
559+
cur.execute(
560+
"CREATE OR REPLACE TEMPORARY TABLE test_binary_list_param "
561+
"(business_uuid BINARY, user_uuid BINARY)"
562+
)
563+
564+
# Insert test data
565+
cur.execute(
566+
"INSERT INTO test_binary_list_param VALUES (%(business_uuid)s, %(user_uuid)s)",
567+
params={
568+
"business_uuid": business_uuid.bytes,
569+
"user_uuid": user_uuid1.bytes,
570+
},
571+
)
572+
573+
# Test single binary parameter (this should work)
574+
results = cur.execute(
575+
"SELECT * FROM test_binary_list_param WHERE business_uuid = %(business_uuid)s",
576+
params={"business_uuid": business_uuid.bytes},
577+
).fetchall()
578+
assert len(results) == 1
579+
580+
# Test binary in list parameter (this was failing with UnicodeDecodeError)
581+
results = cur.execute(
582+
"SELECT * FROM test_binary_list_param "
583+
"WHERE business_uuid = %(business_uuid)s AND user_uuid IN (%(user_uuids)s)",
584+
params={
585+
"business_uuid": business_uuid.bytes,
586+
"user_uuids": [user_uuid1.bytes],
587+
},
588+
).fetchall()
589+
assert len(results) == 1
590+
591+
# Test with multiple binary values in list
592+
results = cur.execute(
593+
"SELECT * FROM test_binary_list_param "
594+
"WHERE user_uuid IN (%(user_uuids)s)",
595+
params={"user_uuids": [user_uuid1.bytes, user_uuid2.bytes]},
596+
).fetchall()
597+
assert len(results) == 1 # Only user_uuid1 exists in the table
598+
599+
finally:
600+
cur.execute("DROP TABLE IF EXISTS test_binary_list_param")

test/unit/test_converter.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import timedelta
55
from decimal import Decimal
66
from logging import getLogger
7+
from uuid import UUID
78

89
import numpy
910
import pytest
@@ -133,3 +134,78 @@ def test_year_month_interval_to_timedelta(months):
133134
assert converter.INTERVAL_YEAR_MONTH_to_numpy_timedelta(
134135
months, scale=0
135136
) == numpy.timedelta64(months, "M")
137+
138+
139+
def test_converter_to_snowflake_bytes():
140+
"""Test that bytes in a list are properly hex-encoded."""
141+
uuid = UUID("12345678-1234-5678-1234-567812345678")
142+
143+
converter = SnowflakeConverter()
144+
# After fix for GH-2311: bytes are now properly hex-encoded via to_snowflake()
145+
assert converter.to_snowflake([uuid.bytes]) == [
146+
"X'12345678123456781234567812345678'"
147+
]
148+
149+
150+
def test_converter_list_with_binary_to_snowflake():
151+
"""Test that bytes values in lists are properly converted to hex format.
152+
153+
Regression test for GitHub Issue #2311.
154+
When binary data (bytes) is passed in a list parameter, each item in the list
155+
should have to_snowflake called on it, converting bytes to hex format.
156+
Without this fix, a UnicodeDecodeError occurs when bytes contain non-ASCII values.
157+
"""
158+
# Use a UUID that generates non-ASCII bytes (contains bytes > 127)
159+
test_uuid = UUID("4363f57d-c9ca-4e63-92c7-3e67786c8a30")
160+
161+
converter = SnowflakeConverter()
162+
163+
# Single bytes should work - this calls _bytes_to_snowflake which hex-encodes
164+
single_result = converter.to_snowflake(test_uuid.bytes)
165+
# The result should be hex-encoded bytes (ASCII safe)
166+
assert isinstance(single_result, (bytes, bytearray))
167+
expected_hex = single_result.decode("ascii")
168+
assert expected_hex == "4363F57DC9CA4E6392C73E67786C8A30"
169+
170+
# List of bytes should also work - currently fails with UnicodeDecodeError
171+
# because _list_to_snowflake doesn't call to_snowflake on each item.
172+
# After fix, each item in the list should be properly quoted with hex format.
173+
list_result = converter.to_snowflake([test_uuid.bytes])
174+
assert list_result == [f"X'{expected_hex}'"]
175+
176+
177+
def test_converter_list_with_non_ascii_bytes_to_snowflake():
178+
"""Test that non-ASCII bytes in lists don't cause UnicodeDecodeError.
179+
180+
Regression test for GitHub Issue #2311.
181+
This test specifically uses bytes that contain values > 127 which cannot
182+
be decoded as ASCII, ensuring the fix properly hex-encodes them first.
183+
"""
184+
# These bytes contain values that cannot be decoded as ASCII
185+
non_ascii_bytes = bytes([0xF5, 0xAB, 0xCD, 0xEF])
186+
187+
converter = SnowflakeConverter()
188+
189+
# Single bytes should work
190+
single_result = converter.to_snowflake(non_ascii_bytes)
191+
assert single_result == b"F5ABCDEF"
192+
193+
# List of non-ASCII bytes should also work without UnicodeDecodeError
194+
list_result = converter.to_snowflake([non_ascii_bytes])
195+
assert list_result == ["X'F5ABCDEF'"]
196+
197+
198+
def test_converter_list_with_mixed_types_to_snowflake():
199+
"""Test that lists with mixed types including bytes work correctly."""
200+
converter = SnowflakeConverter()
201+
202+
# Mix of strings and bytes
203+
mixed_list = [b"\x00\xFF", "hello", 42, None, True]
204+
result = converter.to_snowflake(mixed_list)
205+
206+
# Verify each element is properly converted
207+
assert "X'00FF'" in result # bytes should be hex-encoded
208+
assert "'hello'" in result # string should be quoted
209+
assert "42" in result # number should be stringified
210+
assert "NULL" in result # None should become NULL
211+
assert "TRUE" in result # bool should become TRUE/FALSE

0 commit comments

Comments
 (0)