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.
Files changed (204) hide show
  1. snowflake/connector/__init__.py +148 -0
  2. snowflake/connector/_common/__init__.py +0 -0
  3. snowflake/connector/_common/extras.py +134 -0
  4. snowflake/connector/_common/telemetry.py +221 -0
  5. snowflake/connector/_common/wif_util.py +92 -0
  6. snowflake/connector/_core/sf_core_python.cp315-win_amd64.pyd +0 -0
  7. snowflake/connector/_core/sf_core_python.pyi +79 -0
  8. snowflake/connector/_internal/__init__.py +6 -0
  9. snowflake/connector/_internal/_private_key_helper.py +31 -0
  10. snowflake/connector/_internal/api_client/__init__.py +93 -0
  11. snowflake/connector/_internal/api_client/bridge.py +41 -0
  12. snowflake/connector/_internal/api_client/client_api.py +976 -0
  13. snowflake/connector/_internal/arrow.py +47 -0
  14. snowflake/connector/_internal/arrow_context.py +240 -0
  15. snowflake/connector/_internal/arrow_stream_async.py +266 -0
  16. snowflake/connector/_internal/arrow_stream_iterator.cp311-win_amd64.pyd +0 -0
  17. snowflake/connector/_internal/arrow_stream_iterator.cp312-win_amd64.pyd +0 -0
  18. snowflake/connector/_internal/arrow_stream_iterator.cp313-win_amd64.pyd +0 -0
  19. snowflake/connector/_internal/arrow_stream_iterator.cp314-win_amd64.pyd +0 -0
  20. snowflake/connector/_internal/arrow_stream_iterator.cp314t-win_amd64.pyd +0 -0
  21. snowflake/connector/_internal/arrow_stream_iterator.cp315-win_amd64.pyd +0 -0
  22. snowflake/connector/_internal/arrow_stream_iterator.pyi +42 -0
  23. snowflake/connector/_internal/arrow_stream_utils.py +171 -0
  24. snowflake/connector/_internal/authenticator_type.py +53 -0
  25. snowflake/connector/_internal/backward_compatibility.py +245 -0
  26. snowflake/connector/_internal/binding_converters.py +741 -0
  27. snowflake/connector/_internal/config_utils.py +71 -0
  28. snowflake/connector/_internal/connection/__init__.py +39 -0
  29. snowflake/connector/_internal/connection/connection.py +616 -0
  30. snowflake/connector/_internal/connection/connection_types.py +13 -0
  31. snowflake/connector/_internal/connection/constants.py +25 -0
  32. snowflake/connector/_internal/connection/decorators.py +31 -0
  33. snowflake/connector/_internal/connection/freezable_proxy.py +104 -0
  34. snowflake/connector/_internal/connection/queries.py +10 -0
  35. snowflake/connector/_internal/connection_config_mixin.py +755 -0
  36. snowflake/connector/_internal/cursor/__init__.py +25 -0
  37. snowflake/connector/_internal/cursor/base.py +732 -0
  38. snowflake/connector/_internal/cursor/cursor_types.py +13 -0
  39. snowflake/connector/_internal/cursor/decorators.py +56 -0
  40. snowflake/connector/_internal/cursor/query_result.py +202 -0
  41. snowflake/connector/_internal/cursor/query_result_waiter.py +98 -0
  42. snowflake/connector/_internal/cursor/result_metadata.py +255 -0
  43. snowflake/connector/_internal/cursor/result_set_wrapper.py +66 -0
  44. snowflake/connector/_internal/decorators.py +539 -0
  45. snowflake/connector/_internal/error_kinds.py +111 -0
  46. snowflake/connector/_internal/errorcode.py +35 -0
  47. snowflake/connector/_internal/errorhandler.py +278 -0
  48. snowflake/connector/_internal/fips.py +28 -0
  49. snowflake/connector/_internal/logging/__init__.py +52 -0
  50. snowflake/connector/_internal/logging/config.py +134 -0
  51. snowflake/connector/_internal/logging/core_logger.py +218 -0
  52. snowflake/connector/_internal/logging/native_extension_logger.py +144 -0
  53. snowflake/connector/_internal/logout_config_mapping.py +187 -0
  54. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrayConverter.cpp +57 -0
  55. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrayConverter.hpp +29 -0
  56. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrowTableConverter.cpp +943 -0
  57. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ArrowTableConverter.hpp +63 -0
  58. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BinaryConverter.cpp +18 -0
  59. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BinaryConverter.hpp +26 -0
  60. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BooleanConverter.cpp +21 -0
  61. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/BooleanConverter.hpp +23 -0
  62. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowIterator.cpp +118 -0
  63. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowIterator.hpp +116 -0
  64. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamIterator.cpp +323 -0
  65. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamIterator.hpp +133 -0
  66. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamTableIterator.cpp +275 -0
  67. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/CArrowStreamTableIterator.hpp +168 -0
  68. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ConverterUtil.cpp +439 -0
  69. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ConverterUtil.hpp +30 -0
  70. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DateConverter.cpp +48 -0
  71. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DateConverter.hpp +46 -0
  72. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecFloatConverter.cpp +76 -0
  73. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecFloatConverter.hpp +35 -0
  74. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecimalConverter.cpp +90 -0
  75. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/DecimalConverter.hpp +63 -0
  76. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.cpp +62 -0
  77. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FixedSizeListConverter.hpp +28 -0
  78. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FloatConverter.cpp +29 -0
  79. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/FloatConverter.hpp +35 -0
  80. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IColumnConverter.hpp +17 -0
  81. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntConverter.cpp +23 -0
  82. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntConverter.hpp +41 -0
  83. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntervalConverter.cpp +65 -0
  84. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/IntervalConverter.hpp +54 -0
  85. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/LICENSE.txt +209 -0
  86. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/MapConverter.cpp +79 -0
  87. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/MapConverter.hpp +30 -0
  88. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ObjectConverter.cpp +52 -0
  89. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/ObjectConverter.hpp +29 -0
  90. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Common.cpp +8 -0
  91. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Common.hpp +117 -0
  92. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Helpers.cpp +54 -0
  93. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Python/Helpers.hpp +34 -0
  94. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/SnowflakeType.cpp +33 -0
  95. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/SnowflakeType.hpp +48 -0
  96. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/StringConverter.cpp +18 -0
  97. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/StringConverter.hpp +26 -0
  98. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeConverter.cpp +39 -0
  99. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeConverter.hpp +31 -0
  100. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeStampConverter.cpp +326 -0
  101. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/TimeStampConverter.hpp +138 -0
  102. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/macros.hpp +14 -0
  103. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/time.cpp +65 -0
  104. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/Util/time.hpp +68 -0
  105. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/arrow_stream_iterator.cpp +13278 -0
  106. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/arrow_stream_iterator.pyx +204 -0
  107. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_accessors.h +109 -0
  108. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_alloc.h +125 -0
  109. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_assert.h +45 -0
  110. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_builder.h +1914 -0
  111. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_emitter.h +208 -0
  112. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_endian.h +128 -0
  113. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_epilogue.h +7 -0
  114. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_flatbuffers.h +58 -0
  115. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_identifier.h +144 -0
  116. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_iov.h +31 -0
  117. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_prologue.h +8 -0
  118. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_refmap.h +144 -0
  119. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_rtconfig.h +160 -0
  120. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_types.h +98 -0
  121. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/flatcc_verifier.h +243 -0
  122. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/flatcc_portable.h +14 -0
  123. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/paligned_alloc.h +207 -0
  124. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pattributes.h +80 -0
  125. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic.h +83 -0
  126. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_pop.h +20 -0
  127. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pdiagnostic_push.h +51 -0
  128. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian.h +201 -0
  129. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pendian_detect.h +108 -0
  130. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinline.h +19 -0
  131. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pinttypes.h +52 -0
  132. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable.h +2 -0
  133. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/portable_basic.h +25 -0
  134. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstatic_assert.h +67 -0
  135. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdalign.h +164 -0
  136. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pstdint.h +926 -0
  137. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/punaligned.h +182 -0
  138. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pversion.h +6 -0
  139. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc/portable/pwarnings.h +52 -0
  140. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/flatcc.c +3052 -0
  141. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.c +3019 -0
  142. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.h +3354 -0
  143. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow.hpp +344 -0
  144. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.c +29305 -0
  145. snowflake/connector/_internal/nanoarrow_cpp/ArrowIterator/nanoarrow_ipc.h +381 -0
  146. snowflake/connector/_internal/nanoarrow_cpp/Logging/logging.cpp +108 -0
  147. snowflake/connector/_internal/nanoarrow_cpp/Logging/logging.hpp +47 -0
  148. snowflake/connector/_internal/nanoarrow_cpp/scripts/.clang-format +8 -0
  149. snowflake/connector/_internal/nanoarrow_cpp/scripts/format.sh +8 -0
  150. snowflake/connector/_internal/protobuf_gen/__init__.py +0 -0
  151. snowflake/connector/_internal/protobuf_gen/database_driver_v1_pb2.py +408 -0
  152. snowflake/connector/_internal/protobuf_gen/database_driver_v1_pb2.pyi +4487 -0
  153. snowflake/connector/_internal/protobuf_gen/database_driver_v1_services.py +2088 -0
  154. snowflake/connector/_internal/protobuf_gen/proto_exception.py +17 -0
  155. snowflake/connector/_internal/result_batch.py +150 -0
  156. snowflake/connector/_internal/snowflake_restful.py +222 -0
  157. snowflake/connector/_internal/snowpark_backcompat_helpers.py +31 -0
  158. snowflake/connector/_internal/sqlstate.py +11 -0
  159. snowflake/connector/_internal/statement_utils.py +207 -0
  160. snowflake/connector/_internal/text_utils.py +112 -0
  161. snowflake/connector/_internal/type_codes.py +139 -0
  162. snowflake/connector/_internal/utils.py +65 -0
  163. snowflake/connector/_internal/write_pandas_operation.py +657 -0
  164. snowflake/connector/aio/README.md +168 -0
  165. snowflake/connector/aio/__init__.py +37 -0
  166. snowflake/connector/aio/connection/__init__.py +17 -0
  167. snowflake/connector/aio/connection/_connection.py +506 -0
  168. snowflake/connector/aio/cursor/__init__.py +35 -0
  169. snowflake/connector/aio/cursor/_base.py +1034 -0
  170. snowflake/connector/aio/cursor/_dict_cursor.py +78 -0
  171. snowflake/connector/aio/cursor/_result_set_wrapper.py +35 -0
  172. snowflake/connector/aio/cursor/_snowflake_cursor.py +72 -0
  173. snowflake/connector/aio/pandas_tools.py +279 -0
  174. snowflake/connector/aio/result_batch.py +165 -0
  175. snowflake/connector/auth/__init__.py +0 -0
  176. snowflake/connector/auth/workload_identity.py +5 -0
  177. snowflake/connector/compat.py +22 -0
  178. snowflake/connector/config_manager.py +392 -0
  179. snowflake/connector/connection/__init__.py +30 -0
  180. snowflake/connector/connection/_connection.py +641 -0
  181. snowflake/connector/connection_config.py +517 -0
  182. snowflake/connector/constants.py +52 -0
  183. snowflake/connector/cursor/__init__.py +37 -0
  184. snowflake/connector/cursor/_base.py +1169 -0
  185. snowflake/connector/cursor/_chunked_download_reader.py +75 -0
  186. snowflake/connector/cursor/_dict_cursor.py +78 -0
  187. snowflake/connector/cursor/_result_set_wrapper.py +35 -0
  188. snowflake/connector/cursor/_snowflake_cursor.py +72 -0
  189. snowflake/connector/description.py +7 -0
  190. snowflake/connector/errors.py +492 -0
  191. snowflake/connector/network.py +71 -0
  192. snowflake/connector/pandas_tools.py +191 -0
  193. snowflake/connector/py.typed +0 -0
  194. snowflake/connector/result_batch.py +216 -0
  195. snowflake/connector/telemetry.py +40 -0
  196. snowflake/connector/time_util.py +11 -0
  197. snowflake/connector/types.py +134 -0
  198. snowflake/connector/util_text.py +3 -0
  199. snowflake/connector/version.py +23 -0
  200. snowflake_connector_python-5.2.0.dist-info/DELVEWHEEL +2 -0
  201. snowflake_connector_python-5.2.0.dist-info/METADATA +190 -0
  202. snowflake_connector_python-5.2.0.dist-info/RECORD +204 -0
  203. snowflake_connector_python-5.2.0.dist-info/WHEEL +4 -0
  204. 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