snowflake-connector-python 5.2.0__cp315-cp315-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snowflake/connector/__init__.py +148 -0
- snowflake/connector/_common/__init__.py +0 -0
- snowflake/connector/_common/extras.py +134 -0
- snowflake/connector/_common/telemetry.py +221 -0
- snowflake/connector/_common/wif_util.py +92 -0
- snowflake/connector/_core/sf_core_python.cp315-win_amd64.pyd +0 -0
- snowflake/connector/_core/sf_core_python.pyi +79 -0
- snowflake/connector/_internal/__init__.py +6 -0
- snowflake/connector/_internal/_private_key_helper.py +31 -0
- snowflake/connector/_internal/api_client/__init__.py +93 -0
- snowflake/connector/_internal/api_client/bridge.py +41 -0
- snowflake/connector/_internal/api_client/client_api.py +976 -0
- snowflake/connector/_internal/arrow.py +47 -0
- snowflake/connector/_internal/arrow_context.py +240 -0
- snowflake/connector/_internal/arrow_stream_async.py +266 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp311-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp312-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp313-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp314-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp314t-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.cp315-win_amd64.pyd +0 -0
- snowflake/connector/_internal/arrow_stream_iterator.pyi +42 -0
- snowflake/connector/_internal/arrow_stream_utils.py +171 -0
- snowflake/connector/_internal/authenticator_type.py +53 -0
- snowflake/connector/_internal/backward_compatibility.py +245 -0
- snowflake/connector/_internal/binding_converters.py +741 -0
- snowflake/connector/_internal/config_utils.py +71 -0
- snowflake/connector/_internal/connection/__init__.py +39 -0
- snowflake/connector/_internal/connection/connection.py +616 -0
- snowflake/connector/_internal/connection/connection_types.py +13 -0
- snowflake/connector/_internal/connection/constants.py +25 -0
- snowflake/connector/_internal/connection/decorators.py +31 -0
- snowflake/connector/_internal/connection/freezable_proxy.py +104 -0
- snowflake/connector/_internal/connection/queries.py +10 -0
- snowflake/connector/_internal/connection_config_mixin.py +755 -0
- snowflake/connector/_internal/cursor/__init__.py +25 -0
- snowflake/connector/_internal/cursor/base.py +732 -0
- snowflake/connector/_internal/cursor/cursor_types.py +13 -0
- snowflake/connector/_internal/cursor/decorators.py +56 -0
- snowflake/connector/_internal/cursor/query_result.py +202 -0
- snowflake/connector/_internal/cursor/query_result_waiter.py +98 -0
- snowflake/connector/_internal/cursor/result_metadata.py +255 -0
- snowflake/connector/_internal/cursor/result_set_wrapper.py +66 -0
- snowflake/connector/_internal/decorators.py +539 -0
- snowflake/connector/_internal/error_kinds.py +111 -0
- snowflake/connector/_internal/errorcode.py +35 -0
- snowflake/connector/_internal/errorhandler.py +278 -0
- snowflake/connector/_internal/fips.py +28 -0
- snowflake/connector/_internal/logging/__init__.py +52 -0
- snowflake/connector/_internal/logging/config.py +134 -0
- snowflake/connector/_internal/logging/core_logger.py +218 -0
- snowflake/connector/_internal/logging/native_extension_logger.py +144 -0
- snowflake/connector/_internal/logout_config_mapping.py +187 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +57 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrowTableConverter.cpp +943 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrowTableConverter.hpp +63 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +18 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +118 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +116 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamIterator.cpp +323 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamIterator.hpp +133 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamTableIterator.cpp +275 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamTableIterator.hpp +168 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ConverterUtil.cpp +439 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ConverterUtil.hpp +30 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +48 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +76 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +90 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +63 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +62 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +29 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +41 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +65 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +54 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +79 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +52 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +117 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +54 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +34 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +33 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +18 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +39 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +326 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +138 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/arrow_stream_iterator.cpp +13278 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/arrow_stream_iterator.pyx +204 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +109 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +125 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1914 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +208 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +128 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +58 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +144 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +160 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +98 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +243 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +207 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +80 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +83 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +201 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +108 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +164 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +926 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +182 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc.c +3052 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3019 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3354 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +344 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +29305 -0
- snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +381 -0
- snowflake/connector/_internal/nanoarrow_cpp/Logging/logging.cpp +108 -0
- snowflake/connector/_internal/nanoarrow_cpp/Logging/logging.hpp +47 -0
- snowflake/connector/_internal/nanoarrow_cpp/scripts/.clang-format +8 -0
- snowflake/connector/_internal/nanoarrow_cpp/scripts/format.sh +8 -0
- snowflake/connector/_internal/protobuf_gen/__init__.py +0 -0
- snowflake/connector/_internal/protobuf_gen/database_driver_v1_pb2.py +408 -0
- snowflake/connector/_internal/protobuf_gen/database_driver_v1_pb2.pyi +4487 -0
- snowflake/connector/_internal/protobuf_gen/database_driver_v1_services.py +2088 -0
- snowflake/connector/_internal/protobuf_gen/proto_exception.py +17 -0
- snowflake/connector/_internal/result_batch.py +150 -0
- snowflake/connector/_internal/snowflake_restful.py +222 -0
- snowflake/connector/_internal/snowpark_backcompat_helpers.py +31 -0
- snowflake/connector/_internal/sqlstate.py +11 -0
- snowflake/connector/_internal/statement_utils.py +207 -0
- snowflake/connector/_internal/text_utils.py +112 -0
- snowflake/connector/_internal/type_codes.py +139 -0
- snowflake/connector/_internal/utils.py +65 -0
- snowflake/connector/_internal/write_pandas_operation.py +657 -0
- snowflake/connector/aio/README.md +168 -0
- snowflake/connector/aio/__init__.py +37 -0
- snowflake/connector/aio/connection/__init__.py +17 -0
- snowflake/connector/aio/connection/_connection.py +506 -0
- snowflake/connector/aio/cursor/__init__.py +35 -0
- snowflake/connector/aio/cursor/_base.py +1034 -0
- snowflake/connector/aio/cursor/_dict_cursor.py +78 -0
- snowflake/connector/aio/cursor/_result_set_wrapper.py +35 -0
- snowflake/connector/aio/cursor/_snowflake_cursor.py +72 -0
- snowflake/connector/aio/pandas_tools.py +279 -0
- snowflake/connector/aio/result_batch.py +165 -0
- snowflake/connector/auth/__init__.py +0 -0
- snowflake/connector/auth/workload_identity.py +5 -0
- snowflake/connector/compat.py +22 -0
- snowflake/connector/config_manager.py +392 -0
- snowflake/connector/connection/__init__.py +30 -0
- snowflake/connector/connection/_connection.py +641 -0
- snowflake/connector/connection_config.py +517 -0
- snowflake/connector/constants.py +52 -0
- snowflake/connector/cursor/__init__.py +37 -0
- snowflake/connector/cursor/_base.py +1169 -0
- snowflake/connector/cursor/_chunked_download_reader.py +75 -0
- snowflake/connector/cursor/_dict_cursor.py +78 -0
- snowflake/connector/cursor/_result_set_wrapper.py +35 -0
- snowflake/connector/cursor/_snowflake_cursor.py +72 -0
- snowflake/connector/description.py +7 -0
- snowflake/connector/errors.py +492 -0
- snowflake/connector/network.py +71 -0
- snowflake/connector/pandas_tools.py +191 -0
- snowflake/connector/py.typed +0 -0
- snowflake/connector/result_batch.py +216 -0
- snowflake/connector/telemetry.py +40 -0
- snowflake/connector/time_util.py +11 -0
- snowflake/connector/types.py +134 -0
- snowflake/connector/util_text.py +3 -0
- snowflake/connector/version.py +23 -0
- snowflake_connector_python-5.2.0.dist-info/DELVEWHEEL +2 -0
- snowflake_connector_python-5.2.0.dist-info/METADATA +190 -0
- snowflake_connector_python-5.2.0.dist-info/RECORD +204 -0
- snowflake_connector_python-5.2.0.dist-info/WHEEL +4 -0
- snowflake_connector_python.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter binding serialization for Snowflake universal core.
|
|
3
|
+
|
|
4
|
+
This module handles serialization of Python parameter bindings to JSON format
|
|
5
|
+
for transmission to the Rust core, following the design specified in bindingsdesign.md.
|
|
6
|
+
|
|
7
|
+
It also provides client-side binding support for pyformat/format paramstyles,
|
|
8
|
+
implementing escape(), quote(), and to_snowflake() functions that mirror
|
|
9
|
+
the reference snowflake-connector-python's SnowflakeConverter.
|
|
10
|
+
|
|
11
|
+
Conversion logic mirrors the reference snowflake-connector-python's
|
|
12
|
+
SnowflakeConverter.to_snowflake_bindings and Connection._process_params_qmarks.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import binascii
|
|
18
|
+
import json
|
|
19
|
+
import math
|
|
20
|
+
import time as time_module
|
|
21
|
+
|
|
22
|
+
from collections.abc import Mapping, Sequence
|
|
23
|
+
from datetime import UTC, date, datetime, time, timedelta
|
|
24
|
+
from decimal import Decimal
|
|
25
|
+
from enum import StrEnum
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from .._common.extras import MissingOptionalDependency
|
|
29
|
+
from .._common.extras import numpy as np
|
|
30
|
+
from ..errors import ProgrammingError
|
|
31
|
+
from .errorcode import ER_NOT_SUPPORT_DATA_TYPE
|
|
32
|
+
from .type_codes import PYTHON_TO_SNOWFLAKE_TYPE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParamStyle(StrEnum):
|
|
36
|
+
"""PEP 249 parameter binding style enumeration.
|
|
37
|
+
|
|
38
|
+
``StrEnum`` members behave as their string value — this gives Snowpark's
|
|
39
|
+
``connection.paramstyle.lower()`` for free. Supports parsing from string
|
|
40
|
+
and determining client- vs server-side binding.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
QMARK = "qmark" # Server-side: ? placeholders
|
|
44
|
+
NUMERIC = "numeric" # Server-side: :1, :2 placeholders
|
|
45
|
+
FORMAT = "format" # Client-side: %s interpolation
|
|
46
|
+
PYFORMAT = "pyformat" # Client-side: %(name)s interpolation
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_string(cls, value: str) -> ParamStyle:
|
|
50
|
+
"""Parse ParamStyle from string value, with normalization.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value: Paramstyle string (case-insensitive, whitespace-trimmed)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Matching ParamStyle enum value
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ProgrammingError: If value is not a valid paramstyle
|
|
60
|
+
"""
|
|
61
|
+
normalized = value.strip().lower()
|
|
62
|
+
for style in cls:
|
|
63
|
+
if style.value == normalized:
|
|
64
|
+
return style
|
|
65
|
+
available = [s.value for s in cls]
|
|
66
|
+
raise ProgrammingError(f"Invalid paramstyle: {value!r}. Supported: {', '.join(sorted(available))}")
|
|
67
|
+
|
|
68
|
+
def is_client_side(self) -> bool:
|
|
69
|
+
"""Check if this style uses client-side SQL interpolation."""
|
|
70
|
+
return self in (ParamStyle.FORMAT, ParamStyle.PYFORMAT)
|
|
71
|
+
|
|
72
|
+
def is_server_side(self) -> bool:
|
|
73
|
+
"""Check if this style uses server-side binding."""
|
|
74
|
+
return self in (ParamStyle.QMARK, ParamStyle.NUMERIC)
|
|
75
|
+
|
|
76
|
+
def placeholders(self, n: int) -> str:
|
|
77
|
+
"""Build a comma-separated placeholder string for *n* parameters.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
>>> ParamStyle.QMARK.placeholders(3)
|
|
81
|
+
'?, ?, ?'
|
|
82
|
+
>>> ParamStyle.NUMERIC.placeholders(3)
|
|
83
|
+
':1, :2, :3'
|
|
84
|
+
>>> ParamStyle.FORMAT.placeholders(2)
|
|
85
|
+
'%s, %s'
|
|
86
|
+
"""
|
|
87
|
+
if self.is_client_side():
|
|
88
|
+
return ", ".join("%s" for _ in range(n))
|
|
89
|
+
if self is ParamStyle.NUMERIC:
|
|
90
|
+
return ", ".join(f":{i}" for i in range(1, n + 1))
|
|
91
|
+
return ", ".join("?" for _ in range(n))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Numeric types for IS_NUMERIC check (mirrors reference connector's compat.py)
|
|
95
|
+
_NUM_DATA_TYPES: tuple[type, ...] = (int, float, Decimal)
|
|
96
|
+
_NUMPY_BOOL_TYPES: tuple[type, ...] = ()
|
|
97
|
+
_NUMPY_DATETIME_TYPES: tuple[type, ...] = ()
|
|
98
|
+
_NUMPY_FLOAT_TYPES: tuple[type, ...] = ()
|
|
99
|
+
|
|
100
|
+
if not isinstance(np, MissingOptionalDependency):
|
|
101
|
+
_NUM_DATA_TYPES = _NUM_DATA_TYPES + (
|
|
102
|
+
np.int8,
|
|
103
|
+
np.int16,
|
|
104
|
+
np.int32,
|
|
105
|
+
np.int64,
|
|
106
|
+
np.float16,
|
|
107
|
+
np.float32,
|
|
108
|
+
np.float64,
|
|
109
|
+
np.uint8,
|
|
110
|
+
np.uint16,
|
|
111
|
+
np.uint32,
|
|
112
|
+
np.uint64,
|
|
113
|
+
np.bool_,
|
|
114
|
+
)
|
|
115
|
+
_NUMPY_BOOL_TYPES = (np.bool_,)
|
|
116
|
+
_NUMPY_DATETIME_TYPES = (np.datetime64,)
|
|
117
|
+
_NUMPY_FLOAT_TYPES = (np.float16, np.float32, np.float64)
|
|
118
|
+
|
|
119
|
+
_FLOAT_TYPES: tuple[type, ...] = (float,) + _NUMPY_FLOAT_TYPES
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _is_numeric(value: Any) -> bool:
|
|
123
|
+
"""Check if value is a numeric type."""
|
|
124
|
+
return isinstance(value, _NUM_DATA_TYPES)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _format_float(value: Any) -> str:
|
|
128
|
+
"""Format a float-like value for Snowflake REAL/DOUBLE binding.
|
|
129
|
+
|
|
130
|
+
The server's bind-value parser requires the capitalized literals
|
|
131
|
+
"NaN"/"Infinity"/"-Infinity"; str() on a float produces the lowercase
|
|
132
|
+
short forms "nan"/"inf"/"-inf", which the parser rejects.
|
|
133
|
+
"""
|
|
134
|
+
if math.isnan(value):
|
|
135
|
+
return "NaN"
|
|
136
|
+
if math.isinf(value):
|
|
137
|
+
return "Infinity" if value > 0 else "-Infinity"
|
|
138
|
+
return str(value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_binary(value: Any) -> bool:
|
|
142
|
+
"""Check if value is a binary type."""
|
|
143
|
+
return isinstance(value, (bytes, bytearray))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Epoch constants (timezone-independent)
|
|
147
|
+
_ZERO_EPOCH_DATE = date(1970, 1, 1)
|
|
148
|
+
_ZERO_EPOCH = datetime.fromtimestamp(0, UTC).replace(tzinfo=None)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
DEFAULT_STAGE_ARRAY_BINDING_THRESHOLD = 65280
|
|
152
|
+
Params = Sequence[Any]
|
|
153
|
+
|
|
154
|
+
# Bulk (CSV/stage) date insertion overflows server-side once the epoch-millisecond
|
|
155
|
+
# value reaches this bound (dates after ~year 2969), so such dates are encoded as
|
|
156
|
+
# epoch nanoseconds instead. Mirrors the reference connector's threshold in
|
|
157
|
+
# Converter._date_to_snowflake_bindings_in_bulk_insertion.
|
|
158
|
+
_DATE_BULK_INSERTION_MS_OVERFLOW = 31_536_000_000_000
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def parse_stage_binding_threshold(raw: Any) -> int:
|
|
162
|
+
"""Parse CLIENT_STAGE_ARRAY_BINDING_THRESHOLD; default 65280 when unset/invalid."""
|
|
163
|
+
try:
|
|
164
|
+
return int(raw)
|
|
165
|
+
except (TypeError, ValueError):
|
|
166
|
+
return DEFAULT_STAGE_ARRAY_BINDING_THRESHOLD
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class BindingConverterBase:
|
|
170
|
+
"""Shared Python-to-Snowflake value conversion for JSON and CSV bindings."""
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _convert_datetime_to_epoch_nanoseconds(dt: datetime) -> str:
|
|
174
|
+
"""Convert datetime to epoch nanoseconds string.
|
|
175
|
+
|
|
176
|
+
Uses timedelta arithmetic from a fixed epoch (timezone-independent),
|
|
177
|
+
matching the reference connector's convert_datetime_to_epoch approach.
|
|
178
|
+
"""
|
|
179
|
+
if dt.tzinfo is not None:
|
|
180
|
+
# Convert tz-aware datetime to UTC, then strip tzinfo
|
|
181
|
+
dt = dt.astimezone(UTC).replace(tzinfo=None)
|
|
182
|
+
epoch_seconds = (dt - _ZERO_EPOCH).total_seconds()
|
|
183
|
+
# Format with full precision, remove dot, append "000" for nanoseconds
|
|
184
|
+
return f"{epoch_seconds:f}".replace(".", "") + "000"
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _convert_date_to_epoch_milliseconds(d: date) -> str:
|
|
188
|
+
"""Convert date to epoch milliseconds string.
|
|
189
|
+
|
|
190
|
+
Uses timezone-independent timedelta arithmetic from epoch date. This is
|
|
191
|
+
the representation used by inline JSON (non-bulk) date binding, matching
|
|
192
|
+
the reference connector's ``_date_to_snowflake_bindings``. The CSV/stage
|
|
193
|
+
(bulk) path uses ``_convert_date_for_bulk_insertion`` instead, which
|
|
194
|
+
guards against a server-side overflow for far-future dates.
|
|
195
|
+
"""
|
|
196
|
+
return f"{(d - _ZERO_EPOCH_DATE).total_seconds():.3f}".replace(".", "")
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _convert_date_to_epoch_nanoseconds(d: date) -> str:
|
|
200
|
+
"""Convert date to epoch nanoseconds string (bulk-insertion overflow path)."""
|
|
201
|
+
return f"{(d - _ZERO_EPOCH_DATE).total_seconds():.9f}".replace(".", "")
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def _convert_date_for_bulk_insertion(cls, d: date) -> str:
|
|
205
|
+
"""Convert date for CSV/stage (bulk) insertion.
|
|
206
|
+
|
|
207
|
+
Returns epoch milliseconds, switching to epoch nanoseconds once the
|
|
208
|
+
millisecond value reaches the server's overflow bound (dates after
|
|
209
|
+
~year 2969). Mirrors the reference connector's
|
|
210
|
+
``_date_to_snowflake_bindings_in_bulk_insertion``: without this guard a
|
|
211
|
+
far-future date staged as CSV overflows server-side and round-trips to a
|
|
212
|
+
wrong value, diverging from the inline-JSON path for the same input.
|
|
213
|
+
"""
|
|
214
|
+
milliseconds = cls._convert_date_to_epoch_milliseconds(d)
|
|
215
|
+
if int(milliseconds) < _DATE_BULK_INSERTION_MS_OVERFLOW:
|
|
216
|
+
return milliseconds
|
|
217
|
+
return cls._convert_date_to_epoch_nanoseconds(d)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _convert_time_to_nanoseconds(t: time) -> str:
|
|
221
|
+
"""Convert time to nanoseconds since midnight string.
|
|
222
|
+
|
|
223
|
+
Uses string concatenation approach matching the reference connector's
|
|
224
|
+
_convert_time_to_epoch_nanoseconds.
|
|
225
|
+
"""
|
|
226
|
+
total_seconds = t.hour * 3600 + t.minute * 60 + t.second
|
|
227
|
+
return str(total_seconds) + f"{t.microsecond:06d}" + "000"
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _convert_timedelta_to_nanoseconds(td: timedelta) -> str:
|
|
231
|
+
"""Convert timedelta to nanoseconds string for TIME binding.
|
|
232
|
+
|
|
233
|
+
Matches the reference connector's _timedelta_to_snowflake_bindings.
|
|
234
|
+
"""
|
|
235
|
+
hours, remainder = divmod(td.seconds, 3600)
|
|
236
|
+
mins, secs = divmod(remainder, 60)
|
|
237
|
+
hours += td.days * 24
|
|
238
|
+
return str(hours * 3600 + mins * 60 + secs) + f"{td.microseconds:06d}" + "000"
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def _get_type_and_binding(cls, value: Any) -> tuple[str, Any]:
|
|
242
|
+
"""Resolve the Snowflake type and serialized value for a parameter.
|
|
243
|
+
|
|
244
|
+
Supports explicit type-hint tuples ``("SNOWFLAKE_TYPE", value)`` as used
|
|
245
|
+
by the reference connector (e.g. ``("DECFLOAT", Decimal("1.23"))``).
|
|
246
|
+
When *value* is a 2-tuple whose first element is a string, the tuple is
|
|
247
|
+
unpacked and the explicit type is used; otherwise, the type is inferred
|
|
248
|
+
via ``_convert_value``.
|
|
249
|
+
"""
|
|
250
|
+
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], str):
|
|
251
|
+
snowflake_type, raw_value = value
|
|
252
|
+
_, converted = cls._convert_value(raw_value)
|
|
253
|
+
return snowflake_type, converted
|
|
254
|
+
return cls._convert_value(value)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def _convert_value(cls, value: Any) -> tuple[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Convert a Python value to Snowflake binding format.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Tuple of (Snowflake type string, converted value)
|
|
263
|
+
"""
|
|
264
|
+
if value is None:
|
|
265
|
+
return "ANY", None
|
|
266
|
+
|
|
267
|
+
type_name = value.__class__.__name__.lower()
|
|
268
|
+
snowflake_type = PYTHON_TO_SNOWFLAKE_TYPE.get(type_name)
|
|
269
|
+
if snowflake_type is None:
|
|
270
|
+
if isinstance(value, _NUMPY_BOOL_TYPES):
|
|
271
|
+
snowflake_type = "BOOLEAN"
|
|
272
|
+
elif isinstance(value, _NUMPY_FLOAT_TYPES):
|
|
273
|
+
snowflake_type = "REAL"
|
|
274
|
+
elif _is_numeric(value):
|
|
275
|
+
snowflake_type = "FIXED"
|
|
276
|
+
elif _is_binary(value):
|
|
277
|
+
snowflake_type = "BINARY"
|
|
278
|
+
else:
|
|
279
|
+
snowflake_type = "TEXT"
|
|
280
|
+
|
|
281
|
+
# Convert value to string representation for JSON.
|
|
282
|
+
# Order matters: bool/numpy.bool_ before _is_numeric (bool is subclass
|
|
283
|
+
# of int and numpy.bool_ is in _NUM_DATA_TYPES),
|
|
284
|
+
# datetime before date (datetime is subclass of date).
|
|
285
|
+
if isinstance(value, bool) or isinstance(value, _NUMPY_BOOL_TYPES):
|
|
286
|
+
converted = str(value).lower()
|
|
287
|
+
elif isinstance(value, datetime):
|
|
288
|
+
converted = cls._convert_datetime_to_epoch_nanoseconds(value)
|
|
289
|
+
elif isinstance(value, date):
|
|
290
|
+
converted = cls._convert_date_to_epoch_milliseconds(value)
|
|
291
|
+
elif isinstance(value, time):
|
|
292
|
+
converted = cls._convert_time_to_nanoseconds(value)
|
|
293
|
+
elif isinstance(value, timedelta):
|
|
294
|
+
converted = cls._convert_timedelta_to_nanoseconds(value)
|
|
295
|
+
elif isinstance(value, time_module.struct_time):
|
|
296
|
+
dt = datetime.fromtimestamp(time_module.mktime(value))
|
|
297
|
+
converted = cls._convert_datetime_to_epoch_nanoseconds(dt)
|
|
298
|
+
elif isinstance(value, _FLOAT_TYPES):
|
|
299
|
+
converted = _format_float(value)
|
|
300
|
+
elif _is_numeric(value):
|
|
301
|
+
converted = str(value)
|
|
302
|
+
elif isinstance(value, str):
|
|
303
|
+
converted = value
|
|
304
|
+
elif _is_binary(value):
|
|
305
|
+
converted = binascii.hexlify(value).decode("utf-8")
|
|
306
|
+
else:
|
|
307
|
+
raise ProgrammingError(
|
|
308
|
+
msg=(
|
|
309
|
+
f"Binding data in type ({type_name}) is not supported. "
|
|
310
|
+
"Convert the value to a supported Python type (str, int, float, bool, "
|
|
311
|
+
"bytes, date/datetime/time/timedelta) before binding it."
|
|
312
|
+
),
|
|
313
|
+
errno=ER_NOT_SUPPORT_DATA_TYPE,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return snowflake_type, converted
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def _convert_array(cls, values: list[Any]) -> tuple[str, list[Any]]:
|
|
320
|
+
"""
|
|
321
|
+
Convert an array of Python values to Snowflake binding format.
|
|
322
|
+
|
|
323
|
+
Supports explicit type-hint tuples via ``_get_type_and_binding``.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Tuple of (Snowflake type string, list of converted values)
|
|
327
|
+
"""
|
|
328
|
+
if not values:
|
|
329
|
+
return "TEXT", []
|
|
330
|
+
|
|
331
|
+
# Convert all values and determine type
|
|
332
|
+
converted_values = []
|
|
333
|
+
types = set()
|
|
334
|
+
|
|
335
|
+
for value in values:
|
|
336
|
+
snowflake_type, converted = cls._get_type_and_binding(value)
|
|
337
|
+
converted_values.append(converted)
|
|
338
|
+
if value is not None:
|
|
339
|
+
types.add(snowflake_type)
|
|
340
|
+
|
|
341
|
+
# If all non-null values have the same type, use that type
|
|
342
|
+
# Otherwise, default to TEXT
|
|
343
|
+
if len(types) == 1:
|
|
344
|
+
snowflake_type = types.pop()
|
|
345
|
+
elif len(types) == 0:
|
|
346
|
+
snowflake_type = "TEXT"
|
|
347
|
+
else:
|
|
348
|
+
# Mixed types - use TEXT as fallback
|
|
349
|
+
snowflake_type = "TEXT"
|
|
350
|
+
|
|
351
|
+
return snowflake_type, converted_values
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def _array_binding_row_count(cls, params: Params) -> int:
|
|
355
|
+
row_count = 1
|
|
356
|
+
for value in params:
|
|
357
|
+
if isinstance(value, list):
|
|
358
|
+
if row_count == 1:
|
|
359
|
+
row_count = len(value)
|
|
360
|
+
elif len(value) != row_count:
|
|
361
|
+
# Width validation happens in _build_array_binding_params; keep this pure.
|
|
362
|
+
row_count = len(value)
|
|
363
|
+
return row_count
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def effective_binding_cells(cls, params: Params) -> int:
|
|
367
|
+
"""Return row_count × param_count for array-binding threshold checks."""
|
|
368
|
+
if not params:
|
|
369
|
+
return 0
|
|
370
|
+
return cls._array_binding_row_count(params) * len(params)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class JsonBindingConverter(BindingConverterBase):
|
|
374
|
+
"""Converts Python parameters to Snowflake binding JSON for server-side binding."""
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def serialize_parameters(cls, params: Sequence[Any] | None) -> tuple[str | None, int]:
|
|
378
|
+
"""
|
|
379
|
+
Serialize parameters to JSON format for binding.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
params: Parameters to serialize (sequence for positional)
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Tuple of (JSON string or None, length in bytes)
|
|
386
|
+
"""
|
|
387
|
+
if params is None or len(params) == 0:
|
|
388
|
+
return None, 0
|
|
389
|
+
|
|
390
|
+
bindings = cls._process_params(params)
|
|
391
|
+
if not bindings:
|
|
392
|
+
return None, 0
|
|
393
|
+
|
|
394
|
+
json_str = json.dumps(bindings)
|
|
395
|
+
return json_str, len(json_str.encode("utf-8"))
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
def _process_params(cls, params: Sequence[Any]) -> dict[str, dict[str, Any]]:
|
|
399
|
+
"""
|
|
400
|
+
Process parameters into Snowflake binding format.
|
|
401
|
+
|
|
402
|
+
The format is:
|
|
403
|
+
{
|
|
404
|
+
"1": {"type": "FIXED", "value": "123"},
|
|
405
|
+
"2": {"type": "TEXT", "value": "hello"}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
For arrays (multi-row):
|
|
409
|
+
{
|
|
410
|
+
"1": {"type": "FIXED", "value": ["1", "2", "3"]},
|
|
411
|
+
"2": {"type": "TEXT", "value": ["hello", "world", "foo"]}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
Supports explicit type-hint tuples: ("SNOWFLAKE_TYPE", value).
|
|
415
|
+
This mirrors the reference connector's _get_snowflake_type_and_binding.
|
|
416
|
+
"""
|
|
417
|
+
bindings = {}
|
|
418
|
+
|
|
419
|
+
# Positional parameters (e.g., ? or :1 style)
|
|
420
|
+
for idx, value in enumerate(params):
|
|
421
|
+
if isinstance(value, list):
|
|
422
|
+
# Array binding for bulk operations
|
|
423
|
+
snowflake_type, values = cls._convert_array(value)
|
|
424
|
+
bindings[str(idx + 1)] = {"type": snowflake_type, "value": values}
|
|
425
|
+
else:
|
|
426
|
+
snowflake_type, snowflake_value = cls._get_type_and_binding(value)
|
|
427
|
+
bindings[str(idx + 1)] = {"type": snowflake_type, "value": snowflake_value}
|
|
428
|
+
|
|
429
|
+
return bindings
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class CsvBindingConverter(BindingConverterBase):
|
|
433
|
+
"""Converts Python parameters to Snowflake binding CSV for stage upload."""
|
|
434
|
+
|
|
435
|
+
@classmethod
|
|
436
|
+
def _param_value_at(cls, params: Params, row_idx: int, col_idx: int) -> Any:
|
|
437
|
+
value = params[col_idx]
|
|
438
|
+
if isinstance(value, list):
|
|
439
|
+
return value[row_idx]
|
|
440
|
+
return value
|
|
441
|
+
|
|
442
|
+
@staticmethod
|
|
443
|
+
def _append_escaped_csv_cell(out: list[str], text: str) -> None:
|
|
444
|
+
"""RFC-4180 cell quoting matching ODBC ``append_escaped_csv_cell``.
|
|
445
|
+
|
|
446
|
+
Every non-None cell is unconditionally quoted. The legacy connector
|
|
447
|
+
quoted only when the value contained ``"``, newline, comma, or backslash;
|
|
448
|
+
both approaches are functionally equivalent under
|
|
449
|
+
``field_optionally_enclosed_by='"'``: the critical NULL-vs-empty-string
|
|
450
|
+
distinction (empty string → ``""``; NULL → unquoted empty) is preserved
|
|
451
|
+
either way. Unconditional quoting produces slightly larger upload
|
|
452
|
+
payloads but avoids a conditional branch and matches the ODBC driver.
|
|
453
|
+
"""
|
|
454
|
+
out.append('"')
|
|
455
|
+
out.append(text.replace('"', '""'))
|
|
456
|
+
out.append('"')
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _stage_csv_clock_value(value: Any) -> Any:
|
|
460
|
+
"""Clock-string payload for stage CSV TIME / TIMESTAMP_TZ cells.
|
|
461
|
+
|
|
462
|
+
Inline JSON TIME is nanoseconds since midnight. Stage CSV is loaded
|
|
463
|
+
with ``COPY`` into a TIME column, which does not accept that integer.
|
|
464
|
+
The reference connector's ``to_csv_bindings`` therefore routes TIME
|
|
465
|
+
and TIMESTAMP_TZ through ``to_snowflake`` (``HH:MM:SS[.ffffff]``,
|
|
466
|
+
timestamps as wall-clock text). A naive datetime bound as
|
|
467
|
+
TIMESTAMP_TZ is treated as UTC, matching that converter.
|
|
468
|
+
"""
|
|
469
|
+
if isinstance(value, datetime) and value.tzinfo is None:
|
|
470
|
+
value = value.replace(tzinfo=UTC)
|
|
471
|
+
return ClientSideBindingConverter.to_snowflake(value)
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def _format_csv_cell(cls, value: Any) -> str:
|
|
475
|
+
if value is None:
|
|
476
|
+
return ""
|
|
477
|
+
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], str):
|
|
478
|
+
snowflake_type, raw = value
|
|
479
|
+
if snowflake_type.upper() in ("TIME", "TIMESTAMP_TZ"):
|
|
480
|
+
converted: Any = cls._stage_csv_clock_value(raw)
|
|
481
|
+
else:
|
|
482
|
+
_, converted = cls._get_type_and_binding(value)
|
|
483
|
+
elif isinstance(value, (time, timedelta)):
|
|
484
|
+
converted = cls._stage_csv_clock_value(value)
|
|
485
|
+
elif isinstance(value, date) and not isinstance(value, datetime):
|
|
486
|
+
converted = cls._convert_date_for_bulk_insertion(value)
|
|
487
|
+
else:
|
|
488
|
+
_, converted = cls._get_type_and_binding(value)
|
|
489
|
+
if converted is None:
|
|
490
|
+
return ""
|
|
491
|
+
parts: list[str] = []
|
|
492
|
+
cls._append_escaped_csv_cell(parts, str(converted))
|
|
493
|
+
return "".join(parts)
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
def serialize_parameters_to_csv(cls, params: Params) -> bytes | None:
|
|
497
|
+
"""Serialize column-major array bindings to CSV bytes for stage upload."""
|
|
498
|
+
if not params:
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
expected_rows: int | None = None
|
|
502
|
+
for col_idx, col in enumerate(params):
|
|
503
|
+
if isinstance(col, list):
|
|
504
|
+
if expected_rows is None:
|
|
505
|
+
expected_rows = len(col)
|
|
506
|
+
elif len(col) != expected_rows:
|
|
507
|
+
raise ProgrammingError(
|
|
508
|
+
msg=f"Array binding column widths must all match: "
|
|
509
|
+
f"expected {expected_rows} rows but column {col_idx} has {len(col)} rows.",
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
row_count = expected_rows if expected_rows is not None else 1
|
|
513
|
+
if row_count == 0:
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
lines: list[str] = []
|
|
517
|
+
for row_idx in range(row_count):
|
|
518
|
+
cells = [
|
|
519
|
+
cls._format_csv_cell(cls._param_value_at(params, row_idx, col_idx)) for col_idx in range(len(params))
|
|
520
|
+
]
|
|
521
|
+
lines.append(",".join(cells))
|
|
522
|
+
|
|
523
|
+
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ClientSideBindingConverter:
|
|
527
|
+
"""Converts Python values for client-side SQL interpolation (pyformat/format styles).
|
|
528
|
+
|
|
529
|
+
This class mirrors the reference snowflake-connector-python's SnowflakeConverter
|
|
530
|
+
for client-side binding, implementing escape(), quote(), and to_snowflake() methods.
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def escape(value: Any) -> Any:
|
|
535
|
+
"""Escape special characters in string values for SQL interpolation.
|
|
536
|
+
|
|
537
|
+
Mirrors reference connector's SnowflakeConverter.escape() method.
|
|
538
|
+
"""
|
|
539
|
+
if isinstance(value, list):
|
|
540
|
+
return value
|
|
541
|
+
if value is None or _is_numeric(value) or _is_binary(value):
|
|
542
|
+
return value
|
|
543
|
+
res = value
|
|
544
|
+
res = res.replace("\\", "\\\\")
|
|
545
|
+
res = res.replace("\n", "\\n")
|
|
546
|
+
res = res.replace("\r", "\\r")
|
|
547
|
+
res = res.replace("\047", "\134\047") # single quotes: ' -> \'
|
|
548
|
+
return res
|
|
549
|
+
|
|
550
|
+
@classmethod
|
|
551
|
+
def quote(cls, value: Any) -> str:
|
|
552
|
+
"""Quote a value for SQL interpolation.
|
|
553
|
+
|
|
554
|
+
Mirrors reference connector's SnowflakeConverter.quote() method.
|
|
555
|
+
"""
|
|
556
|
+
if isinstance(value, list):
|
|
557
|
+
# Quote each item in the list and join with commas
|
|
558
|
+
# Apply escape() to each item since to_snowflake() already converted them
|
|
559
|
+
return ",".join(cls.quote(cls.escape(item)) for item in value)
|
|
560
|
+
if value is None:
|
|
561
|
+
return "NULL"
|
|
562
|
+
elif isinstance(value, bool):
|
|
563
|
+
return "TRUE" if value else "FALSE"
|
|
564
|
+
elif isinstance(value, _FLOAT_TYPES):
|
|
565
|
+
return _format_float(value)
|
|
566
|
+
elif _is_numeric(value):
|
|
567
|
+
return str(value)
|
|
568
|
+
elif _is_binary(value):
|
|
569
|
+
# Binary literal syntax: X'hex_value'
|
|
570
|
+
return "X'{}'".format(binascii.hexlify(value).decode("ascii"))
|
|
571
|
+
return f"'{value}'"
|
|
572
|
+
|
|
573
|
+
@classmethod
|
|
574
|
+
def to_snowflake(cls, value: Any) -> Any:
|
|
575
|
+
"""Convert Python value to Snowflake-compatible format for client-side binding.
|
|
576
|
+
|
|
577
|
+
Mirrors reference connector's SnowflakeConverter.to_snowflake() method.
|
|
578
|
+
"""
|
|
579
|
+
if value is None:
|
|
580
|
+
return None
|
|
581
|
+
elif isinstance(value, bool):
|
|
582
|
+
return value
|
|
583
|
+
elif _is_numeric(value):
|
|
584
|
+
return value
|
|
585
|
+
elif isinstance(value, str):
|
|
586
|
+
return value
|
|
587
|
+
elif _is_binary(value):
|
|
588
|
+
return value
|
|
589
|
+
elif isinstance(value, datetime):
|
|
590
|
+
return cls._datetime_to_snowflake(value)
|
|
591
|
+
elif isinstance(value, date):
|
|
592
|
+
return cls._date_to_snowflake(value)
|
|
593
|
+
elif isinstance(value, time):
|
|
594
|
+
return cls._time_to_snowflake(value)
|
|
595
|
+
elif isinstance(value, timedelta):
|
|
596
|
+
return cls._timedelta_to_snowflake(value)
|
|
597
|
+
elif isinstance(value, time_module.struct_time):
|
|
598
|
+
return cls._struct_time_to_snowflake(value)
|
|
599
|
+
elif isinstance(value, _NUMPY_DATETIME_TYPES):
|
|
600
|
+
return str(value) + "+00:00"
|
|
601
|
+
elif isinstance(value, list):
|
|
602
|
+
# List for IN clause - convert each element
|
|
603
|
+
return [cls.to_snowflake(v) for v in value]
|
|
604
|
+
else:
|
|
605
|
+
raise ProgrammingError(
|
|
606
|
+
msg=(
|
|
607
|
+
f"Binding data in type ({value.__class__.__name__.lower()}) is not supported. "
|
|
608
|
+
"Convert the value to a supported Python type (str, int, float, bool, "
|
|
609
|
+
"bytes, date/datetime/time/timedelta) before binding it."
|
|
610
|
+
),
|
|
611
|
+
errno=ER_NOT_SUPPORT_DATA_TYPE,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
@staticmethod
|
|
615
|
+
def _datetime_to_snowflake(value: datetime) -> str:
|
|
616
|
+
"""Convert datetime to Snowflake string format."""
|
|
617
|
+
tzinfo_value = value.tzinfo
|
|
618
|
+
if tzinfo_value:
|
|
619
|
+
# Get UTC offset
|
|
620
|
+
td = tzinfo_value.utcoffset(value)
|
|
621
|
+
if td is None:
|
|
622
|
+
td = timedelta(0)
|
|
623
|
+
sign = "+" if td >= timedelta(0) else "-"
|
|
624
|
+
td_secs = int(td.total_seconds())
|
|
625
|
+
h, m = divmod(abs(td_secs // 60), 60)
|
|
626
|
+
if value.microsecond:
|
|
627
|
+
return (
|
|
628
|
+
f"{value.year:d}-{value.month:02d}-{value.day:02d} "
|
|
629
|
+
f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}."
|
|
630
|
+
f"{value.microsecond:06d}{sign}{h:02d}:{m:02d}"
|
|
631
|
+
)
|
|
632
|
+
return (
|
|
633
|
+
f"{value.year:d}-{value.month:02d}-{value.day:02d} "
|
|
634
|
+
f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}"
|
|
635
|
+
f"{sign}{h:02d}:{m:02d}"
|
|
636
|
+
)
|
|
637
|
+
else:
|
|
638
|
+
if value.microsecond:
|
|
639
|
+
return (
|
|
640
|
+
f"{value.year:d}-{value.month:02d}-{value.day:02d} "
|
|
641
|
+
f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}."
|
|
642
|
+
f"{value.microsecond:06d}"
|
|
643
|
+
)
|
|
644
|
+
return (
|
|
645
|
+
f"{value.year:d}-{value.month:02d}-{value.day:02d} "
|
|
646
|
+
f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
@staticmethod
|
|
650
|
+
def _date_to_snowflake(value: date) -> str:
|
|
651
|
+
"""Convert date to Snowflake string format."""
|
|
652
|
+
return f"{value.year:d}-{value.month:02d}-{value.day:02d}"
|
|
653
|
+
|
|
654
|
+
@staticmethod
|
|
655
|
+
def _time_to_snowflake(value: time) -> str:
|
|
656
|
+
"""Convert time to Snowflake string format."""
|
|
657
|
+
if value.microsecond:
|
|
658
|
+
return f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}.{value.microsecond:06d}"
|
|
659
|
+
return f"{value.hour:02d}:{value.minute:02d}:{value.second:02d}"
|
|
660
|
+
|
|
661
|
+
@staticmethod
|
|
662
|
+
def _timedelta_to_snowflake(value: timedelta) -> str:
|
|
663
|
+
"""Convert timedelta to Snowflake string format."""
|
|
664
|
+
hours, remainder = divmod(value.seconds, 3600)
|
|
665
|
+
mins, secs = divmod(remainder, 60)
|
|
666
|
+
hours += value.days * 24
|
|
667
|
+
if value.microseconds:
|
|
668
|
+
return f"{hours:02d}:{mins:02d}:{secs:02d}.{value.microseconds:06d}"
|
|
669
|
+
return f"{hours:02d}:{mins:02d}:{secs:02d}"
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def _struct_time_to_snowflake(value: time_module.struct_time) -> str:
|
|
673
|
+
"""Convert struct_time to Snowflake string format."""
|
|
674
|
+
return (
|
|
675
|
+
f"{value.tm_year:d}-{value.tm_mon:02d}-{value.tm_mday:02d} "
|
|
676
|
+
f"{value.tm_hour:02d}:{value.tm_min:02d}:{value.tm_sec:02d}"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
@classmethod
|
|
680
|
+
def process_single_param(cls, param: Any) -> Any:
|
|
681
|
+
"""Process a single parameter for client-side binding.
|
|
682
|
+
|
|
683
|
+
Applies to_snowflake -> escape -> quote transformation.
|
|
684
|
+
"""
|
|
685
|
+
return cls.quote(cls.escape(cls.to_snowflake(param)))
|
|
686
|
+
|
|
687
|
+
@classmethod
|
|
688
|
+
def process_params_pyformat(
|
|
689
|
+
cls, params: Sequence[Any] | Mapping[str, Any] | None
|
|
690
|
+
) -> dict[str, Any] | tuple[Any, ...]:
|
|
691
|
+
"""Process parameters for pyformat/format style binding.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
params: Parameters as sequence (for %s) or mapping (for %(name)s)
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
Processed parameters ready for % string interpolation
|
|
698
|
+
"""
|
|
699
|
+
if params is None:
|
|
700
|
+
return ()
|
|
701
|
+
|
|
702
|
+
if isinstance(params, Mapping):
|
|
703
|
+
# Named parameters: %(name)s
|
|
704
|
+
return {key: cls.process_single_param(value) for key, value in params.items()}
|
|
705
|
+
else:
|
|
706
|
+
# Positional parameters: %s
|
|
707
|
+
return tuple(cls.process_single_param(param) for param in params)
|
|
708
|
+
|
|
709
|
+
@classmethod
|
|
710
|
+
def interpolate_query(
|
|
711
|
+
cls,
|
|
712
|
+
query: str,
|
|
713
|
+
params: Sequence[Any] | Mapping[str, Any] | None,
|
|
714
|
+
*,
|
|
715
|
+
interpolate_empty_sequences: bool = False,
|
|
716
|
+
) -> str:
|
|
717
|
+
"""Interpolate parameters into query using Python % formatting.
|
|
718
|
+
|
|
719
|
+
This is the main entry point for client-side binding, mirroring
|
|
720
|
+
the reference connector's _preprocess_pyformat_query method.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
query: SQL query with %s or %(name)s placeholders
|
|
724
|
+
params: Parameters to interpolate
|
|
725
|
+
interpolate_empty_sequences: When True, perform ``query % params``
|
|
726
|
+
even when *params* is empty. This unescapes ``%%`` → ``%``
|
|
727
|
+
which is required when SQLAlchemy doubles percent signs during
|
|
728
|
+
compilation (pyformat paramstyle). Matches the old connector's
|
|
729
|
+
``connection._interpolate_empty_sequences`` flag.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
Query string with parameters interpolated
|
|
733
|
+
"""
|
|
734
|
+
if params is None:
|
|
735
|
+
return query
|
|
736
|
+
|
|
737
|
+
processed_params = cls.process_params_pyformat(params)
|
|
738
|
+
|
|
739
|
+
if interpolate_empty_sequences or len(processed_params) > 0:
|
|
740
|
+
return query % processed_params
|
|
741
|
+
return query
|