mirror of
https://github.com/hanzo-ds/connect.git
synced 2026-10-30 20:31:24 +00:00
255 lines
11 KiB
Python
255 lines
11 KiB
Python
import ipaddress
|
|
import uuid
|
|
import zoneinfo
|
|
from datetime import date, datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from clickhouse_connect.driver import tzutil
|
|
from clickhouse_connect.driver.binding import (
|
|
DT64Param,
|
|
_extract_tz_from_type,
|
|
bind_query,
|
|
finalize_query,
|
|
format_bind_value,
|
|
format_query_value,
|
|
)
|
|
|
|
|
|
def test_finalize():
|
|
hash_id = "0x772"
|
|
timestamp = datetime.fromtimestamp(1661447719)
|
|
parameters = {"hash_id": hash_id, "dt": timestamp}
|
|
expected = "SELECT hash_id FROM db.mytable WHERE hash_id = '0x772' AND dt = '2022-08-25 17:15:19'"
|
|
query = finalize_query("SELECT hash_id FROM db.mytable WHERE hash_id = %(hash_id)s AND dt = %(dt)s", parameters)
|
|
assert query == expected
|
|
|
|
parameters = [hash_id, timestamp]
|
|
query = finalize_query("SELECT hash_id FROM db.mytable WHERE hash_id = %s AND dt = %s", parameters)
|
|
assert query == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value, expected",
|
|
[
|
|
("a", "a"),
|
|
("a'", r"a\'"),
|
|
("'a'", r"\'a\'"),
|
|
("''a'", r"\'\'a\'"),
|
|
([], "[]"),
|
|
([1], "[1]"),
|
|
(["a"], "['a']"),
|
|
(["a'"], r"['a\'']"),
|
|
([["a"]], "[['a']]"),
|
|
(date(2023, 6, 1), "2023-06-01"),
|
|
(datetime(2023, 6, 1, 20, 4, 5), "2023-06-01 20:04:05"),
|
|
([date(2023, 6, 1), date(2023, 8, 5)], "['2023-06-01', '2023-08-05']"),
|
|
(b"AB", r"\x41\x42"),
|
|
(b"\x00\xf8'", r"\x00\xf8\x27"),
|
|
([b"AB"], r"['\x41\x42']"),
|
|
((b"AB", "x"), r"('\x41\x42', 'x')"),
|
|
(uuid.UUID("019e1780-3b41-7673-a645-17f9b60fe8ec"), "019e1780-3b41-7673-a645-17f9b60fe8ec"),
|
|
(
|
|
[uuid.UUID("019e1780-3b41-7673-a645-17f9b60fe8ec"), uuid.UUID("019e1780-3b41-7673-a645-17f9b60fe8ed")],
|
|
"['019e1780-3b41-7673-a645-17f9b60fe8ec', '019e1780-3b41-7673-a645-17f9b60fe8ed']",
|
|
),
|
|
((uuid.UUID("019e1780-3b41-7673-a645-17f9b60fe8ec"), "user_1"), "('019e1780-3b41-7673-a645-17f9b60fe8ec', 'user_1')"),
|
|
(ipaddress.IPv4Address("10.13.79.1"), "10.13.79.1"),
|
|
([ipaddress.IPv4Address("10.13.79.1")], "['10.13.79.1']"),
|
|
(ipaddress.IPv6Address("2001:db8::79"), "2001:db8::79"),
|
|
([ipaddress.IPv6Address("2001:db8::79")], "['2001:db8::79']"),
|
|
],
|
|
)
|
|
def test_format_bind_value(value, expected):
|
|
assert format_bind_value(value) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value, expected",
|
|
[
|
|
(b"AB", r"'\x41\x42'"),
|
|
(bytearray(b"AB"), r"'\x41\x42'"),
|
|
(b"j!lUA\xf8\x93q;ky\x00", r"'\x6a\x21\x6c\x55\x41\xf8\x93\x71\x3b\x6b\x79\x00'"),
|
|
([b"AB", b"\x00"], r"['\x41\x42', '\x00']"),
|
|
((b"AB", 1), r"('\x41\x42', 1)"),
|
|
],
|
|
)
|
|
def test_format_query_value_bytes(value, expected):
|
|
assert format_query_value(value) == expected
|
|
|
|
|
|
def test_finalize_bytes():
|
|
query = finalize_query("INSERT INTO t (id) VALUES (%(id)s)", {"id": b"j!lUA\xf8\x93q;ky\x00"})
|
|
assert query == r"INSERT INTO t (id) VALUES ('\x6a\x21\x6c\x55\x41\xf8\x93\x71\x3b\x6b\x79\x00')"
|
|
|
|
|
|
class TestBindQueryTimezoneHint:
|
|
"""Type hint timezone in {param:Type} should override server_tz."""
|
|
|
|
berlin_tz = zoneinfo.ZoneInfo("Europe/Berlin")
|
|
dt_utc = datetime(2025, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
def test_datetime64_utc_hint(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime64(6, 'UTC')}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.000000"
|
|
|
|
def test_datetime_utc_hint(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime('UTC')}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00"
|
|
|
|
def test_dt64param_utc_hint(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime64(6, 'UTC')}"
|
|
_, params = bind_query(query, {"dt_64": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.000000"
|
|
|
|
def test_no_hint_tz_falls_back_to_server_tz(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime64(6)}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 13:00:00.000000"
|
|
|
|
def test_nullable_wrapper(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:Nullable(DateTime64(6, 'UTC'))}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.000000"
|
|
|
|
def test_lowcardinality_nullable_wrapper(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:LowCardinality(Nullable(DateTime('UTC')))}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00"
|
|
|
|
def test_array_container(self):
|
|
query = "SELECT * FROM t WHERE dt IN {dts:Array(DateTime64(6, 'UTC'))}"
|
|
_, params = bind_query(query, {"dts": [self.dt_utc]}, server_tz=self.berlin_tz)
|
|
assert "2025-01-01 12:00:00" in params["param_dts"]
|
|
|
|
def test_tuple_container(self):
|
|
query = "SELECT * FROM t WHERE x = {val:Tuple(DateTime('UTC'), String)}"
|
|
_, params = bind_query(query, {"val": (self.dt_utc, "test")}, server_tz=self.berlin_tz)
|
|
assert "2025-01-01 12:00:00" in params["param_val"]
|
|
|
|
def test_map_type_hint_extraction(self):
|
|
tz = _extract_tz_from_type("Map(String, DateTime64(6, 'UTC'))")
|
|
assert tzutil.is_utc_timezone(tz)
|
|
|
|
def test_non_utc_hint(self):
|
|
tokyo_tz = zoneinfo.ZoneInfo("Asia/Tokyo")
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime('Asia/Tokyo')}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
# 12:00 UTC = 21:00 Tokyo
|
|
expected = self.dt_utc.astimezone(tokyo_tz).strftime("%Y-%m-%d %H:%M:%S")
|
|
assert params["param_dt"] == expected
|
|
|
|
def test_unknown_tz_falls_back_to_server_tz(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:DateTime('FakeZone/Nowhere')}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 13:00:00"
|
|
|
|
def test_malformed_hint_falls_back_to_server_tz(self):
|
|
query = "SELECT * FROM t WHERE dt >= {dt:NotAType!!!}"
|
|
_, params = bind_query(query, {"dt": self.dt_utc}, server_tz=self.berlin_tz)
|
|
assert params["param_dt"] == "2025-01-01 13:00:00"
|
|
|
|
|
|
class TestBindQueryDateTime64Precision:
|
|
"""A DateTime64 type hint preserves sub-second precision without a _64 suffix or DT64Param."""
|
|
|
|
utc = timezone.utc
|
|
dt = datetime(2025, 1, 1, 12, 0, 0, 250306, tzinfo=timezone.utc)
|
|
|
|
def test_scalar(self):
|
|
query = "SELECT {dt:DateTime64(6)}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_plain_datetime_truncates(self):
|
|
query = "SELECT {dt:DateTime}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00"
|
|
|
|
def test_lowercase_spelling(self):
|
|
query = "SELECT {dt:Datetime64(6)}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_nullable_wrapper(self):
|
|
query = "SELECT {dt:Nullable(DateTime64(9))}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_lowcardinality_nullable_wrapper(self):
|
|
query = "SELECT {dt:LowCardinality(Nullable(DateTime64(6)))}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_none_nullable(self):
|
|
query = "SELECT {dt:Nullable(DateTime64(6))}"
|
|
_, params = bind_query(query, {"dt": None}, server_tz=self.utc)
|
|
assert params["param_dt"] == "\\N"
|
|
|
|
def test_array(self):
|
|
query = "SELECT {dts:Array(DateTime64(6))}"
|
|
dts = [self.dt, self.dt.replace(microsecond=777722)]
|
|
_, params = bind_query(query, {"dts": dts}, server_tz=self.utc)
|
|
assert params["param_dts"] == "['2025-01-01 12:00:00.250306', '2025-01-01 12:00:00.777722']"
|
|
|
|
def test_tuple(self):
|
|
query = "SELECT {val:Tuple(DateTime64(6), String)}"
|
|
_, params = bind_query(query, {"val": (self.dt, "user_1")}, server_tz=self.utc)
|
|
assert params["param_val"] == "('2025-01-01 12:00:00.250306', 'user_1')"
|
|
|
|
def test_array_of_tuple(self):
|
|
query = "SELECT {vals:Array(Tuple(DateTime64(6), String))}"
|
|
_, params = bind_query(query, {"vals": [(self.dt, "user_1")]}, server_tz=self.utc)
|
|
assert params["param_vals"] == "[('2025-01-01 12:00:00.250306', 'user_1')]"
|
|
|
|
def test_tz_hint_with_precision(self):
|
|
berlin = zoneinfo.ZoneInfo("Europe/Berlin")
|
|
query = "SELECT {dt:DateTime64(6, 'Europe/Berlin')}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert params["param_dt"] == self.dt.astimezone(berlin).strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
|
|
def test_lowercase_tz_hint_preserved(self):
|
|
query = "SELECT {dt:Datetime64(6, 'UTC')}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=zoneinfo.ZoneInfo("Europe/Berlin"))
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_already_dt64param_not_double_wrapped(self):
|
|
query = "SELECT {dt:DateTime64(6)}"
|
|
_, params = bind_query(query, {"dt": DT64Param(self.dt)}, server_tz=self.utc)
|
|
assert params["param_dt"] == "2025-01-01 12:00:00.250306"
|
|
|
|
def test_malformed_hint_does_not_crash(self):
|
|
query = "SELECT {dt:DateTime64(((}"
|
|
_, params = bind_query(query, {"dt": self.dt}, server_tz=self.utc)
|
|
assert "param_dt" in params
|
|
|
|
|
|
class TestBindQuerySuffixCollision:
|
|
"""A param whose real name ends in _64 keeps its name when the query binds the full name."""
|
|
|
|
utc = timezone.utc
|
|
dt = datetime(2026, 1, 1, 12, 0, 0, 250306, tzinfo=timezone.utc)
|
|
|
|
def test_scalar(self):
|
|
query = "SELECT 1 WHERE t >= {param_64:DateTime64(6, 'UTC')}"
|
|
_, params = bind_query(query, {"param_64": self.dt}, server_tz=self.utc)
|
|
assert params == {"param_param_64": "2026-01-01 12:00:00.250306"}
|
|
|
|
def test_array(self):
|
|
query = "SELECT {dts_64:Array(DateTime64(6))}"
|
|
_, params = bind_query(query, {"dts_64": [self.dt]}, server_tz=self.utc)
|
|
assert params == {"param_dts_64": "['2026-01-01 12:00:00.250306']"}
|
|
|
|
def test_suffix_strips_when_stripped_name_is_bound(self):
|
|
query = "SELECT {dt:DateTime64(6)}"
|
|
_, params = bind_query(query, {"dt_64": self.dt}, server_tz=self.utc)
|
|
assert params == {"param_dt": "2026-01-01 12:00:00.250306"}
|
|
|
|
def test_suffix_strips_without_placeholders(self):
|
|
query = "SELECT %(dt)s"
|
|
q, params = bind_query(query, {"dt_64": self.dt}, server_tz=self.utc)
|
|
assert q == "SELECT '2026-01-01 12:00:00.250306'"
|
|
assert params == {}
|