arrowsql 0.1.0__cp313-cp313-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 (45) hide show
  1. arrowsql/__init__.py +653 -0
  2. arrowsql/_rust_auth.py +170 -0
  3. arrowsql/_server_sqlstate.py +11 -0
  4. arrowsql/_utf8_strings.py +78 -0
  5. arrowsql/agent.py +620 -0
  6. arrowsql/aio.py +533 -0
  7. arrowsql/arrow_parallel.py +464 -0
  8. arrowsql/async_query/__init__.py +40 -0
  9. arrowsql/async_query/_connection_context.py +39 -0
  10. arrowsql/async_query/_native.py +24 -0
  11. arrowsql/async_query/async_connection.py +160 -0
  12. arrowsql/async_query/async_cursor.py +225 -0
  13. arrowsql/async_query/async_execute.py +113 -0
  14. arrowsql/async_query/async_fetch.py +153 -0
  15. arrowsql/async_query/exception_translator.py +132 -0
  16. arrowsql/auth.py +913 -0
  17. arrowsql/bulk_parallel.py +385 -0
  18. arrowsql/connection.py +2377 -0
  19. arrowsql/connection_string_builder.py +114 -0
  20. arrowsql/connection_string_parser.py +417 -0
  21. arrowsql/constants.py +701 -0
  22. arrowsql/cursor.py +4516 -0
  23. arrowsql/db_connection.py +98 -0
  24. arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
  25. arrowsql/ddbc_bindings.pyi +283 -0
  26. arrowsql/decimal_config.py +98 -0
  27. arrowsql/exceptions.py +378 -0
  28. arrowsql/helpers.py +436 -0
  29. arrowsql/logging.py +700 -0
  30. arrowsql/mssql_python.pyi +579 -0
  31. arrowsql/odbc_provider.py +269 -0
  32. arrowsql/parameter_helper.py +442 -0
  33. arrowsql/perf_timer.py +286 -0
  34. arrowsql/pooling.py +147 -0
  35. arrowsql/py.typed +1 -0
  36. arrowsql/row.py +462 -0
  37. arrowsql/type.py +160 -0
  38. arrowsql-0.1.0.dist-info/METADATA +146 -0
  39. arrowsql-0.1.0.dist-info/RECORD +45 -0
  40. arrowsql-0.1.0.dist-info/WHEEL +4 -0
  41. arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
  42. arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
  44. arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
  45. mssql_python/__init__.py +39 -0
arrowsql/helpers.py ADDED
@@ -0,0 +1,436 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation.
3
+ Licensed under the MIT license.
4
+ This module provides helper functions for the mssql_python package.
5
+ """
6
+
7
+ import re
8
+ import threading
9
+ import locale
10
+ from typing import Any, Union, Tuple, Optional
11
+ from arrowsql import ddbc_bindings
12
+ from arrowsql.exceptions import raise_exception
13
+ from arrowsql.logging import logger
14
+ from arrowsql.constants import ConstantsDDBC
15
+
16
+ # normalize_architecture import removed as it's unused
17
+
18
+ # Diagnostic records from DDBCSQLGetAllDiagRecords carry "[SQLSTATE] (native)".
19
+ _DIAG_STATE_RE = re.compile(r"^\[([0-9A-Z]{5})\] \((-?\d+)\)$")
20
+
21
+
22
+ def _native_error_from_diag_records(handle: Any, sqlstate: str) -> Optional[int]:
23
+ """Recover the SQL Server error number from the handle's diagnostic records.
24
+
25
+ Used when the binding's ErrorInfo has no nativeError field. Prefers the
26
+ record whose SQLSTATE matches the raised one. 0 means "no server error"
27
+ (driver-generated diagnostic) and is reported as None.
28
+ """
29
+ try:
30
+ records = ddbc_bindings.DDBCSQLGetAllDiagRecords(handle)
31
+ except Exception: # pylint: disable=broad-exception-caught
32
+ return None
33
+ if not isinstance(records, list):
34
+ return None
35
+ first = None
36
+ for record in records:
37
+ match = _DIAG_STATE_RE.match(record[0]) if record and isinstance(record[0], str) else None
38
+ if not match:
39
+ continue
40
+ if match.group(1) == sqlstate:
41
+ return int(match.group(2)) or None
42
+ if first is None:
43
+ first = int(match.group(2)) or None
44
+ return first
45
+
46
+
47
+ def check_error(handle_type: int, handle: Any, ret: int) -> None:
48
+ """
49
+ Check for errors and raise an exception if an error is found.
50
+
51
+ Args:
52
+ handle_type: The type of the handle (e.g., SQL_HANDLE_ENV, SQL_HANDLE_DBC).
53
+ handle: The SqlHandle object associated with the operation.
54
+ ret: The return code from the DDBC function call.
55
+
56
+ Raises:
57
+ RuntimeError: If an error is found.
58
+ """
59
+ if ret < 0:
60
+ logger.debug(
61
+ "check_error: Error detected - handle_type=%d, return_code=%d", handle_type, ret
62
+ )
63
+ error_info = ddbc_bindings.DDBCSQLCheckError(handle_type, handle, ret)
64
+ logger.error("Error: %s", error_info.ddbcErrorMsg)
65
+ logger.debug("check_error: SQL state=%s", error_info.sqlState)
66
+ native_error = getattr(error_info, "nativeError", None)
67
+ if native_error is None:
68
+ native_error = _native_error_from_diag_records(handle, error_info.sqlState)
69
+ raise_exception(error_info.sqlState, error_info.ddbcErrorMsg, native_error or None)
70
+
71
+
72
+ def sanitize_connection_string(conn_str: str) -> str:
73
+ """
74
+ Sanitize the connection string by removing sensitive information.
75
+
76
+ Delegates to the parser-based implementation in connection_string_parser
77
+ which correctly handles ODBC braced values (e.g. PWD={Top;Secret}).
78
+
79
+ Args:
80
+ conn_str (str): The connection string to sanitize.
81
+ Returns:
82
+ str: The sanitized connection string.
83
+ """
84
+ from arrowsql.connection_string_parser import (
85
+ sanitize_connection_string as _sanitize,
86
+ )
87
+
88
+ return _sanitize(conn_str)
89
+
90
+
91
+ def _sanitize_text(text: str, max_length: int) -> Tuple[str, bool]:
92
+ """Keep word characters, dash and dot; cap length. Returns (result, was_truncated)."""
93
+ sanitized = re.sub(r"[^\w\-\.]", "", text)
94
+ was_truncated = len(sanitized) > max_length
95
+ if was_truncated:
96
+ sanitized = sanitized[:max_length] + "..."
97
+ return (sanitized if sanitized else "<invalid>"), was_truncated
98
+
99
+
100
+ def sanitize_user_input(user_input: str, max_length: int = 50) -> str:
101
+ """
102
+ Sanitize user input for safe logging by removing control characters,
103
+ limiting length, and ensuring safe characters only.
104
+
105
+ Args:
106
+ user_input (str): The user input to sanitize.
107
+ max_length (int): Maximum length of the sanitized output.
108
+
109
+ Returns:
110
+ str: The sanitized string safe for logging.
111
+ """
112
+ logger.debug(
113
+ "sanitize_user_input: Sanitizing input (type=%s, length=%d)",
114
+ type(user_input).__name__,
115
+ len(user_input) if isinstance(user_input, str) else 0,
116
+ )
117
+ if not isinstance(user_input, str):
118
+ logger.debug("sanitize_user_input: Non-string input detected")
119
+ return "<non-string>"
120
+
121
+ result, was_truncated = _sanitize_text(user_input, max_length)
122
+ logger.debug(
123
+ "sanitize_user_input: Result length=%d, truncated=%s", len(result), str(was_truncated)
124
+ )
125
+ return result
126
+
127
+
128
+ def validate_attribute_value(
129
+ attribute: Union[int, str],
130
+ value: Union[int, str, bytes, bytearray],
131
+ is_connected: bool = True,
132
+ sanitize_logs: bool = True,
133
+ max_log_length: int = 50,
134
+ ) -> Tuple[bool, Optional[str], str, str]:
135
+ """
136
+ Validates attribute and value pairs for connection attributes.
137
+
138
+ Performs basic type checking and validation of ODBC connection attributes.
139
+
140
+ Args:
141
+ attribute (int): The connection attribute to validate (SQL_ATTR_*)
142
+ value: The value to set for the attribute (int, str, bytes, or bytearray)
143
+ is_connected (bool): Whether the connection is already established
144
+ sanitize_logs (bool): Whether to include sanitized versions for logging
145
+ max_log_length (int): Maximum length of sanitized output for logging
146
+
147
+ Returns:
148
+ tuple: (is_valid, error_message, sanitized_attribute, sanitized_value)
149
+ """
150
+ logger.debug(
151
+ "validate_attribute_value: Validating attribute=%s, value_type=%s, is_connected=%s",
152
+ str(attribute),
153
+ type(value).__name__,
154
+ str(is_connected),
155
+ )
156
+
157
+ def _sanitize_for_logging(input_val: Any, max_length: int = max_log_length) -> str:
158
+ if not isinstance(input_val, str):
159
+ try:
160
+ input_val = str(input_val)
161
+ except (TypeError, ValueError):
162
+ return "<non-string>"
163
+ return _sanitize_text(input_val, max_length)[0]
164
+
165
+ # Create sanitized versions for logging
166
+ sanitized_attr = _sanitize_for_logging(attribute) if sanitize_logs else str(attribute)
167
+ sanitized_val = _sanitize_for_logging(value) if sanitize_logs else str(value)
168
+
169
+ # Basic attribute validation - must be an integer
170
+ if not isinstance(attribute, int):
171
+ logger.debug(
172
+ "validate_attribute_value: Attribute not an integer - type=%s", type(attribute).__name__
173
+ )
174
+ return (
175
+ False,
176
+ f"Attribute must be an integer, got {type(attribute).__name__}",
177
+ sanitized_attr,
178
+ sanitized_val,
179
+ )
180
+
181
+ # Define driver-level attributes that are supported
182
+ supported_attributes = [
183
+ ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value,
184
+ ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value,
185
+ ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value,
186
+ ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value,
187
+ ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value,
188
+ ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value,
189
+ ]
190
+
191
+ # Check if attribute is supported
192
+ if attribute not in supported_attributes:
193
+ logger.debug("validate_attribute_value: Unsupported attribute - attr=%d", attribute)
194
+ return (
195
+ False,
196
+ f"Unsupported attribute: {attribute}",
197
+ sanitized_attr,
198
+ sanitized_val,
199
+ )
200
+
201
+ # Check timing constraints for these specific attributes
202
+ before_only_attributes = [
203
+ ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value,
204
+ ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value,
205
+ ]
206
+
207
+ # Check if attribute can be set at the current connection state
208
+ if is_connected and attribute in before_only_attributes:
209
+ logger.debug(
210
+ "validate_attribute_value: Timing violation - attr=%d cannot be set after connection",
211
+ attribute,
212
+ )
213
+ return (
214
+ False,
215
+ (
216
+ f"Attribute {attribute} must be set before connection establishment. "
217
+ "Use the attrs_before parameter when creating the connection."
218
+ ),
219
+ sanitized_attr,
220
+ sanitized_val,
221
+ )
222
+
223
+ # Basic value type validation
224
+ if isinstance(value, int):
225
+ # For integer values, check if negative (login timeout can be -1 for default)
226
+ if value < 0 and attribute != ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value:
227
+ return (
228
+ False,
229
+ f"Integer value cannot be negative: {value}",
230
+ sanitized_attr,
231
+ sanitized_val,
232
+ )
233
+
234
+ elif isinstance(value, str):
235
+ # Basic string length check
236
+ max_string_size = 8192 # 8KB maximum
237
+ if len(value) > max_string_size:
238
+ return (
239
+ False,
240
+ f"String value too large: {len(value)} bytes (max {max_string_size})",
241
+ sanitized_attr,
242
+ sanitized_val,
243
+ )
244
+
245
+ elif isinstance(value, (bytes, bytearray)):
246
+ # Basic binary length check
247
+ max_binary_size = 32768 # 32KB maximum
248
+ if len(value) > max_binary_size:
249
+ return (
250
+ False,
251
+ f"Binary value too large: {len(value)} bytes (max {max_binary_size})",
252
+ sanitized_attr,
253
+ sanitized_val,
254
+ )
255
+
256
+ else:
257
+ # Reject unsupported value types
258
+ return (
259
+ False,
260
+ f"Unsupported attribute value type: {type(value).__name__}",
261
+ sanitized_attr,
262
+ sanitized_val,
263
+ )
264
+
265
+ # All basic validations passed
266
+ logger.debug(
267
+ "validate_attribute_value: Validation passed - attr=%d, value_type=%s",
268
+ attribute,
269
+ type(value).__name__,
270
+ )
271
+ return True, None, sanitized_attr, sanitized_val
272
+
273
+
274
+ _PYCORE_CONNECTION_KEY_MAP = {
275
+ "uid": "user_name",
276
+ "pwd": "password",
277
+ "trusted_connection": "trusted_connection",
278
+ "authentication": "authentication",
279
+ "server": "server",
280
+ "addr": "server",
281
+ "address": "server",
282
+ "database": "database",
283
+ "applicationintent": "application_intent",
284
+ "encrypt": "encryption",
285
+ "trustservercertificate": "trust_server_certificate",
286
+ "trust_server_certificate": "trust_server_certificate",
287
+ "hostnameincertificate": "host_name_in_certificate",
288
+ "servercertificate": "server_certificate",
289
+ "serverspn": "server_spn",
290
+ "multisubnetfailover": "multi_subnet_failover",
291
+ "ipaddresspreference": "ip_address_preference",
292
+ "keepalive": "keep_alive",
293
+ "keepaliveinterval": "keep_alive_interval",
294
+ "packetsize": "packet_size",
295
+ "packet size": "packet_size",
296
+ "connectretrycount": "connect_retry_count",
297
+ "connectretryinterval": "connect_retry_interval",
298
+ }
299
+
300
+ _PYCORE_INTEGER_KEYS = {
301
+ "packet_size",
302
+ "connect_retry_count",
303
+ "connect_retry_interval",
304
+ "keep_alive",
305
+ "keep_alive_interval",
306
+ }
307
+
308
+ _PYCORE_STRICT_INTEGER_RANGES = {
309
+ "connect_retry_count": (0, 255),
310
+ "connect_retry_interval": (1, 60),
311
+ "keep_alive": (0, 2**32 - 1),
312
+ "keep_alive_interval": (0, 2**32 - 1),
313
+ }
314
+
315
+ _PYCORE_SECONDS_TO_MILLISECONDS_KEYS = {
316
+ "keep_alive",
317
+ "keep_alive_interval",
318
+ }
319
+
320
+ _PYCORE_UINT32_MAX = 2**32 - 1
321
+
322
+
323
+ def connstr_to_pycore_params(params: dict, *, strict: bool = False) -> dict:
324
+ """Translate parsed ODBC connection-string parameters for mssql-py-core.
325
+
326
+ Used by async connection setup and by bulk copy when it opens a separate
327
+ mssql-py-core connection. Keys are mapped to py-core's snake_case format,
328
+ numeric strings are converted to integers, and synonym precedence is
329
+ preserved.
330
+
331
+ Unsupported keys and invalid integers are dropped by default. When
332
+ ``strict`` is enabled, they raise ``ValueError`` instead.
333
+ """
334
+ # Only keys listed below are forwarded to py-core.
335
+ # Unknown/reserved keys (app, workstationid, language, connect_timeout,
336
+ # mars_connection) are silently dropped here. In the normal connect()
337
+ # path the parser validates keywords first (validate_keywords=True),
338
+ # but bulkcopy parses with validation off, so this mapping is the
339
+ # authoritative filter in that path.
340
+ pycore_params: dict = {}
341
+ seen_pycore_keys = set()
342
+
343
+ for connstr_key, raw_value in params.items():
344
+ pycore_key = _PYCORE_CONNECTION_KEY_MAP.get(connstr_key)
345
+ if pycore_key is None or raw_value is None:
346
+ continue
347
+
348
+ # First-wins: match ODBC behaviour — first synonym in the
349
+ # connection string takes precedence (e.g. Addr before Server).
350
+ if pycore_key in seen_pycore_keys:
351
+ continue
352
+ seen_pycore_keys.add(pycore_key)
353
+
354
+ # ODBC values are always strings; py-core expects native types for int keys.
355
+ # Boolean params (trust_server_certificate, multi_subnet_failover) are passed
356
+ # as strings — all Yes/No validation is in connection.rs for single-location
357
+ # consistency with Encrypt, ApplicationIntent, IPAddressPreference, etc.
358
+ if pycore_key in _PYCORE_INTEGER_KEYS:
359
+ # Numeric params (timeouts, packet size, etc.) — skip on bad input
360
+ try:
361
+ value = int(raw_value)
362
+ except (ValueError, TypeError):
363
+ if strict:
364
+ raise ValueError(
365
+ f"Connection parameter '{connstr_key}' must be an integer"
366
+ ) from None
367
+ continue
368
+
369
+ if strict and pycore_key in _PYCORE_STRICT_INTEGER_RANGES:
370
+ minimum, maximum = _PYCORE_STRICT_INTEGER_RANGES[pycore_key]
371
+ if not minimum <= value <= maximum:
372
+ raise ValueError(
373
+ f"Connection parameter '{connstr_key}' must be between "
374
+ f"{minimum} and {maximum}"
375
+ )
376
+
377
+ if pycore_key in _PYCORE_SECONDS_TO_MILLISECONDS_KEYS:
378
+ value = min(value * 1000, _PYCORE_UINT32_MAX)
379
+
380
+ pycore_params[pycore_key] = value
381
+ else:
382
+ # String params (server, database, encryption, etc.) — pass through
383
+ pycore_params[pycore_key] = raw_value
384
+
385
+ if strict:
386
+ unsupported = sorted(set(params) - set(_PYCORE_CONNECTION_KEY_MAP))
387
+ if unsupported:
388
+ names = ", ".join(unsupported)
389
+ raise ValueError(
390
+ f"Connection parameters are not supported for mssql-py-core connections: {names}"
391
+ )
392
+
393
+ return pycore_params
394
+
395
+
396
+ # Settings functionality moved here to avoid circular imports
397
+
398
+ # Initialize the locale setting only once at module import time
399
+ # This avoids thread-safety issues with locale
400
+ _default_decimal_separator: str = "."
401
+ try:
402
+ # Get the locale setting once during module initialization
403
+ locale_separator = locale.localeconv()["decimal_point"]
404
+ if locale_separator and len(locale_separator) == 1:
405
+ _default_decimal_separator = locale_separator
406
+ except (AttributeError, KeyError, TypeError, ValueError):
407
+ pass # Keep the default "." if locale access fails
408
+
409
+
410
+ class Settings:
411
+ """
412
+ Settings class for mssql_python package configuration.
413
+
414
+ This class holds global settings that affect the behavior of the package,
415
+ including lowercase column names, decimal separator, and UUID handling.
416
+ """
417
+
418
+ def __init__(self) -> None:
419
+ self.lowercase: bool = False
420
+ # Use the pre-determined separator - no locale access here
421
+ self.decimal_separator: str = _default_decimal_separator
422
+ # Controls whether UNIQUEIDENTIFIER columns return uuid.UUID (True)
423
+ # or str (False). Default True returns native uuid.UUID objects.
424
+ # Set to False to return str for pyodbc-compatible migration.
425
+ self.native_uuid: bool = True
426
+
427
+
428
+ # Global settings instance
429
+ _settings: Settings = Settings()
430
+ _settings_lock: threading.Lock = threading.Lock()
431
+
432
+
433
+ def get_settings() -> Settings:
434
+ """Return the global settings object"""
435
+ with _settings_lock:
436
+ return _settings