fastmssql 0.2.1__cp313-cp313-macosx_11_0_arm64.whl → 0.3.0__cp313-cp313-macosx_11_0_arm64.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.
Potentially problematic release.
This version of fastmssql might be problematic. Click here for more details.
- fastmssql/__init__.py +689 -11
- fastmssql/fastmssql.cpython-313-darwin.so +0 -0
- {fastmssql-0.2.1.dist-info → fastmssql-0.3.0.dist-info}/METADATA +260 -192
- fastmssql-0.3.0.dist-info/RECORD +6 -0
- {fastmssql-0.2.1.dist-info → fastmssql-0.3.0.dist-info}/WHEEL +2 -0
- fastmssql-0.3.0.dist-info/licenses/LICENSE +675 -0
- fastmssql/fastmssql.py +0 -706
- fastmssql/fastmssql_core.cpython-313-darwin.so +0 -0
- fastmssql-0.2.1.dist-info/RECORD +0 -7
- fastmssql-0.2.1.dist-info/licenses/LICENSE +0 -139
fastmssql/fastmssql.py
DELETED
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
High-level Python API for mssql-python-rust
|
|
3
|
-
|
|
4
|
-
This module provides convenient Python functions that wrap the Rust core functionality.
|
|
5
|
-
Supports asynchronous operations only.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterable
|
|
9
|
-
|
|
10
|
-
try:
|
|
11
|
-
# Try to import the compiled Rust module from the package
|
|
12
|
-
from . import fastmssql_core as _core
|
|
13
|
-
except ImportError:
|
|
14
|
-
# Fallback for development - try absolute import
|
|
15
|
-
try:
|
|
16
|
-
import fastmssql_core as _core
|
|
17
|
-
except ImportError as e:
|
|
18
|
-
import sys
|
|
19
|
-
print(f"ERROR: fastmssql_core module not found: {e}")
|
|
20
|
-
print("Solution: Build the extension with 'maturin develop' or 'maturin develop --release'")
|
|
21
|
-
print("Make sure you're in the project root directory and have Rust/Maturin installed.")
|
|
22
|
-
sys.exit(1)
|
|
23
|
-
|
|
24
|
-
class Row:
|
|
25
|
-
"""Python wrapper around Row for better type hints and documentation."""
|
|
26
|
-
|
|
27
|
-
def __init__(self, py_row: Any) -> None:
|
|
28
|
-
"""Initialize with a Row instance.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
py_row: The underlying Rust Row instance
|
|
32
|
-
"""
|
|
33
|
-
self._row = py_row
|
|
34
|
-
|
|
35
|
-
def get(self, column: Union[str, int]) -> Any:
|
|
36
|
-
"""Get value by column name or index.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
column: Column name (str) or index (int)
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
The column value
|
|
43
|
-
"""
|
|
44
|
-
return self._row.get(column)
|
|
45
|
-
|
|
46
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
47
|
-
"""Convert row to dictionary.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
Dictionary mapping column names to values
|
|
51
|
-
"""
|
|
52
|
-
return self._row.to_dict()
|
|
53
|
-
|
|
54
|
-
def to_tuple(self) -> tuple:
|
|
55
|
-
"""Convert row to tuple.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Tuple of column values in order
|
|
59
|
-
"""
|
|
60
|
-
return self._row.to_tuple()
|
|
61
|
-
|
|
62
|
-
def __getitem__(self, key: Union[str, int]) -> Any:
|
|
63
|
-
"""Get value by column name or index."""
|
|
64
|
-
return self._row[key]
|
|
65
|
-
|
|
66
|
-
def __len__(self) -> int:
|
|
67
|
-
"""Get number of columns in the row."""
|
|
68
|
-
return len(self._row)
|
|
69
|
-
|
|
70
|
-
def __repr__(self) -> str:
|
|
71
|
-
"""String representation of the row."""
|
|
72
|
-
return f"Row({self.to_dict()})"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class ExecutionResult:
|
|
76
|
-
"""Python wrapper around ExecutionResult for better type hints."""
|
|
77
|
-
|
|
78
|
-
def __init__(self, py_result):
|
|
79
|
-
"""Initialize with a ExecutionResult instance."""
|
|
80
|
-
self._result = py_result
|
|
81
|
-
|
|
82
|
-
def rows(self) -> List[Row]:
|
|
83
|
-
"""Query result rows (for SELECT queries).
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
List of Row objects
|
|
87
|
-
"""
|
|
88
|
-
if self._result.has_rows():
|
|
89
|
-
# Get raw rows - could be property or method
|
|
90
|
-
try:
|
|
91
|
-
if callable(self._result.rows):
|
|
92
|
-
raw_rows = self._result.rows()
|
|
93
|
-
else:
|
|
94
|
-
raw_rows = self._result.rows
|
|
95
|
-
return [Row(py_row) for py_row in raw_rows]
|
|
96
|
-
except Exception:
|
|
97
|
-
return []
|
|
98
|
-
return []
|
|
99
|
-
|
|
100
|
-
@property
|
|
101
|
-
def affected_rows(self) -> Optional[int]:
|
|
102
|
-
"""Number of affected rows (for INSERT/UPDATE/DELETE).
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
Number of affected rows, or None if not applicable
|
|
106
|
-
"""
|
|
107
|
-
return self._result.affected_rows
|
|
108
|
-
|
|
109
|
-
def has_rows(self) -> bool:
|
|
110
|
-
"""Check if result contains rows.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
True if result has rows (SELECT query), False otherwise
|
|
114
|
-
"""
|
|
115
|
-
return self._result.has_rows()
|
|
116
|
-
|
|
117
|
-
def __len__(self) -> int:
|
|
118
|
-
"""Get number of rows in the result."""
|
|
119
|
-
return len(self.rows())
|
|
120
|
-
|
|
121
|
-
def __iter__(self):
|
|
122
|
-
"""Iterate over rows in the result."""
|
|
123
|
-
return iter(self.rows())
|
|
124
|
-
|
|
125
|
-
def __repr__(self) -> str:
|
|
126
|
-
"""String representation of the result."""
|
|
127
|
-
if self.has_rows():
|
|
128
|
-
return f"ExecutionResult(rows={len(self.rows())})"
|
|
129
|
-
else:
|
|
130
|
-
return f"ExecutionResult(affected_rows={self.affected_rows})"
|
|
131
|
-
|
|
132
|
-
class PoolConfig:
|
|
133
|
-
"""Python wrapper around PoolConfig for better documentation."""
|
|
134
|
-
|
|
135
|
-
def __init__(
|
|
136
|
-
self,
|
|
137
|
-
max_size: int = 10,
|
|
138
|
-
min_idle: Optional[int] = None,
|
|
139
|
-
max_lifetime_secs: Optional[int] = None,
|
|
140
|
-
idle_timeout_secs: Optional[int] = None,
|
|
141
|
-
connection_timeout_secs: Optional[int] = None
|
|
142
|
-
):
|
|
143
|
-
"""Initialize connection pool configuration.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
max_size: Maximum number of connections in pool (default: 10)
|
|
147
|
-
min_idle: Minimum number of idle connections to maintain
|
|
148
|
-
max_lifetime_secs: Maximum lifetime of connections in seconds
|
|
149
|
-
idle_timeout_secs: How long a connection can be idle before being closed (seconds)
|
|
150
|
-
connection_timeout_secs: Timeout for establishing new connections (seconds)
|
|
151
|
-
"""
|
|
152
|
-
self._config = _core.PoolConfig(
|
|
153
|
-
max_size=max_size,
|
|
154
|
-
min_idle=min_idle,
|
|
155
|
-
max_lifetime_secs=max_lifetime_secs,
|
|
156
|
-
idle_timeout_secs=idle_timeout_secs,
|
|
157
|
-
connection_timeout_secs=connection_timeout_secs
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
@property
|
|
161
|
-
def max_size(self) -> int:
|
|
162
|
-
"""Maximum number of connections in pool."""
|
|
163
|
-
return self._config.max_size
|
|
164
|
-
|
|
165
|
-
@property
|
|
166
|
-
def min_idle(self) -> Optional[int]:
|
|
167
|
-
"""Minimum number of idle connections."""
|
|
168
|
-
return self._config.min_idle
|
|
169
|
-
|
|
170
|
-
@property
|
|
171
|
-
def max_lifetime_secs(self) -> Optional[int]:
|
|
172
|
-
"""Maximum lifetime of connections in seconds."""
|
|
173
|
-
return self._config.max_lifetime_secs
|
|
174
|
-
|
|
175
|
-
@property
|
|
176
|
-
def idle_timeout_secs(self) -> Optional[int]:
|
|
177
|
-
"""Idle timeout in seconds."""
|
|
178
|
-
return self._config.idle_timeout_secs
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def connection_timeout_secs(self) -> Optional[int]:
|
|
182
|
-
"""Connection timeout in seconds."""
|
|
183
|
-
return self._config.connection_timeout_secs
|
|
184
|
-
|
|
185
|
-
@staticmethod
|
|
186
|
-
def high_throughput() -> 'PoolConfig':
|
|
187
|
-
"""Create configuration for high-throughput scenarios."""
|
|
188
|
-
config = PoolConfig.__new__(PoolConfig)
|
|
189
|
-
config._config = _core.PoolConfig.high_throughput()
|
|
190
|
-
return config
|
|
191
|
-
|
|
192
|
-
@staticmethod
|
|
193
|
-
def low_resource() -> 'PoolConfig':
|
|
194
|
-
"""Create configuration for low-resource scenarios."""
|
|
195
|
-
config = PoolConfig.__new__(PoolConfig)
|
|
196
|
-
config._config = _core.PoolConfig.low_resource()
|
|
197
|
-
return config
|
|
198
|
-
|
|
199
|
-
@staticmethod
|
|
200
|
-
def development() -> 'PoolConfig':
|
|
201
|
-
"""Create configuration for development scenarios."""
|
|
202
|
-
config = PoolConfig.__new__(PoolConfig)
|
|
203
|
-
config._config = _core.PoolConfig.development()
|
|
204
|
-
return config
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class Parameter:
|
|
208
|
-
"""Represents a SQL parameter with value and optional type information."""
|
|
209
|
-
|
|
210
|
-
# Valid SQL parameter types (base types without parameters)
|
|
211
|
-
VALID_SQL_TYPES = {
|
|
212
|
-
'VARCHAR', 'NVARCHAR', 'CHAR', 'NCHAR', 'TEXT', 'NTEXT',
|
|
213
|
-
'INT', 'BIGINT', 'SMALLINT', 'TINYINT', 'BIT',
|
|
214
|
-
'FLOAT', 'REAL', 'DECIMAL', 'NUMERIC', 'MONEY', 'SMALLMONEY',
|
|
215
|
-
'DATETIME', 'DATETIME2', 'SMALLDATETIME', 'DATE', 'TIME', 'DATETIMEOFFSET',
|
|
216
|
-
'BINARY', 'VARBINARY', 'IMAGE',
|
|
217
|
-
'UNIQUEIDENTIFIER', 'XML', 'JSON'
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
@staticmethod
|
|
221
|
-
def _extract_base_type(sql_type: str) -> str:
|
|
222
|
-
"""Extract the base SQL type from a type specification.
|
|
223
|
-
|
|
224
|
-
Examples:
|
|
225
|
-
VARCHAR(50) -> VARCHAR
|
|
226
|
-
NVARCHAR(MAX) -> NVARCHAR
|
|
227
|
-
DECIMAL(10,2) -> DECIMAL
|
|
228
|
-
INT -> INT
|
|
229
|
-
"""
|
|
230
|
-
# Find the first opening parenthesis and extract everything before it
|
|
231
|
-
paren_pos = sql_type.find('(')
|
|
232
|
-
if paren_pos != -1:
|
|
233
|
-
return sql_type[:paren_pos].strip().upper()
|
|
234
|
-
return sql_type.strip().upper()
|
|
235
|
-
|
|
236
|
-
def __init__(self, value: Any, sql_type: Optional[str] = None) -> None:
|
|
237
|
-
"""Initialize a parameter.
|
|
238
|
-
|
|
239
|
-
Args:
|
|
240
|
-
value: The parameter value (None, bool, int, float, str, bytes, or iterable for IN clauses)
|
|
241
|
-
sql_type: Optional SQL type hint. Can include parameters:
|
|
242
|
-
- 'VARCHAR(50)', 'NVARCHAR(MAX)', 'DECIMAL(10,2)', etc.
|
|
243
|
-
- Base types: 'VARCHAR', 'INT', 'DATETIME', etc.
|
|
244
|
-
|
|
245
|
-
Raises:
|
|
246
|
-
ValueError: If sql_type is provided but the base type is not recognized
|
|
247
|
-
|
|
248
|
-
Note:
|
|
249
|
-
Lists, tuples, sets and other iterables (except strings/bytes) are automatically
|
|
250
|
-
expanded for use in IN clauses. So Parameter([1, 2, 3]) will expand to
|
|
251
|
-
placeholder values for "WHERE id IN (@P1, @P2, @P3)".
|
|
252
|
-
"""
|
|
253
|
-
# Automatically detect iterables for IN clause expansion
|
|
254
|
-
if self._is_iterable_value(value):
|
|
255
|
-
self.value = list(value) # Convert to list for consistency
|
|
256
|
-
self.is_expanded = True
|
|
257
|
-
else:
|
|
258
|
-
self.value = value
|
|
259
|
-
self.is_expanded = False
|
|
260
|
-
|
|
261
|
-
if sql_type is not None:
|
|
262
|
-
# Extract base type and validate it
|
|
263
|
-
base_type = self._extract_base_type(sql_type)
|
|
264
|
-
if base_type not in self.VALID_SQL_TYPES:
|
|
265
|
-
raise ValueError(
|
|
266
|
-
f"Invalid sql_type '{sql_type}'. Base type '{base_type}' not recognized. "
|
|
267
|
-
f"Valid base types: {', '.join(sorted(self.VALID_SQL_TYPES))}"
|
|
268
|
-
)
|
|
269
|
-
# Store the original type specification (including parameters)
|
|
270
|
-
self.sql_type = sql_type.upper()
|
|
271
|
-
else:
|
|
272
|
-
self.sql_type = None
|
|
273
|
-
|
|
274
|
-
@staticmethod
|
|
275
|
-
def _is_iterable_value(value: Any) -> bool:
|
|
276
|
-
"""Check if a value is an iterable that can be expanded for IN clauses.
|
|
277
|
-
|
|
278
|
-
Returns True for lists, tuples, sets, etc., but False for strings and bytes
|
|
279
|
-
which should be treated as single values.
|
|
280
|
-
"""
|
|
281
|
-
return (
|
|
282
|
-
hasattr(value, '__iter__') and
|
|
283
|
-
not isinstance(value, (str, bytes))
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
def __repr__(self) -> str:
|
|
287
|
-
if self.is_expanded:
|
|
288
|
-
type_info = f", type={self.sql_type}" if self.sql_type else ""
|
|
289
|
-
return f"Parameter(IN_values={self.value!r}{type_info})"
|
|
290
|
-
elif self.sql_type:
|
|
291
|
-
return f"Parameter(value={self.value!r}, type={self.sql_type})"
|
|
292
|
-
return f"Parameter(value={self.value!r})"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
class Parameters:
|
|
296
|
-
"""Container for SQL parameters that can be passed to execute()."""
|
|
297
|
-
|
|
298
|
-
def __init__(self, *args, **kwargs):
|
|
299
|
-
"""Initialize parameters container.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
*args: Positional parameter values. Can be individual values or iterables.
|
|
303
|
-
Iterables (lists, tuples, etc.) will be expanded by Rust for performance.
|
|
304
|
-
Strings and bytes are treated as single values, not expanded.
|
|
305
|
-
**kwargs: Named parameter values (for @name placeholders, if supported)
|
|
306
|
-
|
|
307
|
-
Examples:
|
|
308
|
-
# Individual parameters
|
|
309
|
-
params = Parameters(1, "hello", 3.14)
|
|
310
|
-
|
|
311
|
-
# Mix of individual and iterable parameters (expansion handled in Rust)
|
|
312
|
-
params = Parameters(1, [2, 3, 4], "hello") # Rust expands [2,3,4] automatically
|
|
313
|
-
|
|
314
|
-
# All types of iterables work
|
|
315
|
-
params = Parameters([1, 2], (3, 4), {5, 6}) # All expanded by Rust
|
|
316
|
-
"""
|
|
317
|
-
self._positional = []
|
|
318
|
-
self._named = {}
|
|
319
|
-
|
|
320
|
-
# Handle positional parameters - let Rust handle expansion
|
|
321
|
-
for arg in args:
|
|
322
|
-
if isinstance(arg, Parameter):
|
|
323
|
-
self._positional.append(arg)
|
|
324
|
-
else:
|
|
325
|
-
self._positional.append(Parameter(arg))
|
|
326
|
-
|
|
327
|
-
# Handle named parameters
|
|
328
|
-
for name, value in kwargs.items():
|
|
329
|
-
if isinstance(value, Parameter):
|
|
330
|
-
self._named[name] = value
|
|
331
|
-
else:
|
|
332
|
-
self._named[name] = Parameter(value)
|
|
333
|
-
|
|
334
|
-
def add(self, value: Any, sql_type: Optional[str] = None) -> 'Parameters':
|
|
335
|
-
"""Add a positional parameter and return self for chaining.
|
|
336
|
-
|
|
337
|
-
Args:
|
|
338
|
-
value: Parameter value (can be an iterable for automatic expansion by Rust)
|
|
339
|
-
sql_type: Optional SQL type hint
|
|
340
|
-
|
|
341
|
-
Returns:
|
|
342
|
-
Self for method chaining
|
|
343
|
-
|
|
344
|
-
Examples:
|
|
345
|
-
params = Parameters().add(42).add("hello")
|
|
346
|
-
params = Parameters().add([1, 2, 3]) # Rust expands automatically
|
|
347
|
-
"""
|
|
348
|
-
self._positional.append(Parameter(value, sql_type))
|
|
349
|
-
return self
|
|
350
|
-
|
|
351
|
-
def extend(self, other: Union['Parameters', Iterable[Any]]) -> 'Parameters':
|
|
352
|
-
"""Extend parameters with another Parameters object or iterable.
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
other: Another Parameters object or an iterable of values
|
|
356
|
-
|
|
357
|
-
Returns:
|
|
358
|
-
Self for method chaining
|
|
359
|
-
|
|
360
|
-
Examples:
|
|
361
|
-
params1 = Parameters(1, 2)
|
|
362
|
-
params2 = Parameters(3, 4)
|
|
363
|
-
params1.extend(params2) # params1 now has [1, 2, 3, 4]
|
|
364
|
-
|
|
365
|
-
params = Parameters(1, 2)
|
|
366
|
-
params.extend([3, 4, 5]) # params now has [1, 2, [3, 4, 5]] - Rust handles expansion
|
|
367
|
-
"""
|
|
368
|
-
if isinstance(other, Parameters):
|
|
369
|
-
self._positional.extend(other._positional)
|
|
370
|
-
self._named.update(other._named)
|
|
371
|
-
else:
|
|
372
|
-
# Add as single parameter - Rust will expand if it's an iterable
|
|
373
|
-
self._positional.append(Parameter(other))
|
|
374
|
-
return self
|
|
375
|
-
|
|
376
|
-
def set(self, name: str, value: Any, sql_type: Optional[str] = None) -> 'Parameters':
|
|
377
|
-
"""Set a named parameter and return self for chaining.
|
|
378
|
-
|
|
379
|
-
Args:
|
|
380
|
-
name: Parameter name
|
|
381
|
-
value: Parameter value
|
|
382
|
-
sql_type: Optional SQL type hint
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
Self for method chaining
|
|
386
|
-
"""
|
|
387
|
-
self._named[name] = Parameter(value, sql_type)
|
|
388
|
-
return self
|
|
389
|
-
|
|
390
|
-
@property
|
|
391
|
-
def positional(self) -> List[Parameter]:
|
|
392
|
-
"""Get positional parameters."""
|
|
393
|
-
return self._positional.copy()
|
|
394
|
-
|
|
395
|
-
@property
|
|
396
|
-
def named(self) -> Dict[str, Parameter]:
|
|
397
|
-
"""Get named parameters."""
|
|
398
|
-
return self._named.copy()
|
|
399
|
-
|
|
400
|
-
def to_list(self) -> List[Any]:
|
|
401
|
-
"""Convert to simple list of values for compatibility.
|
|
402
|
-
|
|
403
|
-
Note: Iterable expansion is now handled by Rust for performance,
|
|
404
|
-
so this returns the raw values as-is.
|
|
405
|
-
"""
|
|
406
|
-
return [param.value for param in self._positional]
|
|
407
|
-
|
|
408
|
-
def __len__(self) -> int:
|
|
409
|
-
"""Get total number of parameters."""
|
|
410
|
-
return len(self._positional) + len(self._named)
|
|
411
|
-
|
|
412
|
-
def __repr__(self) -> str:
|
|
413
|
-
parts = []
|
|
414
|
-
if self._positional:
|
|
415
|
-
parts.append(f"positional={len(self._positional)}")
|
|
416
|
-
if self._named:
|
|
417
|
-
parts.append(f"named={len(self._named)}")
|
|
418
|
-
return f"Parameters({', '.join(parts)})"
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
class SslConfig:
|
|
422
|
-
"""SSL/TLS configuration for database connections."""
|
|
423
|
-
|
|
424
|
-
def __init__(
|
|
425
|
-
self,
|
|
426
|
-
encryption_level: Optional[str] = None,
|
|
427
|
-
trust_server_certificate: bool = False,
|
|
428
|
-
ca_certificate_path: Optional[str] = None,
|
|
429
|
-
enable_sni: bool = True,
|
|
430
|
-
server_name: Optional[str] = None
|
|
431
|
-
):
|
|
432
|
-
"""Initialize SSL configuration.
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
encryption_level: Encryption level - "Required", "LoginOnly", or "Off" (default: "Required")
|
|
436
|
-
trust_server_certificate: Trust server certificate without validation (dangerous in production)
|
|
437
|
-
ca_certificate_path: Path to custom CA certificate file (.pem, .crt, or .der)
|
|
438
|
-
enable_sni: Enable Server Name Indication (default: True)
|
|
439
|
-
server_name: Custom server name for certificate validation
|
|
440
|
-
|
|
441
|
-
Note:
|
|
442
|
-
trust_server_certificate and ca_certificate_path are mutually exclusive.
|
|
443
|
-
For production, either use a valid certificate in the system trust store,
|
|
444
|
-
or provide a ca_certificate_path to a trusted CA certificate.
|
|
445
|
-
"""
|
|
446
|
-
# Set default encryption level
|
|
447
|
-
if encryption_level is None:
|
|
448
|
-
encryption_level = "Required"
|
|
449
|
-
|
|
450
|
-
# Validate encryption level
|
|
451
|
-
valid_levels = ["Required", "LoginOnly", "Off"]
|
|
452
|
-
if encryption_level not in valid_levels:
|
|
453
|
-
raise ValueError(f"Invalid encryption_level. Must be one of: {valid_levels}")
|
|
454
|
-
|
|
455
|
-
# Map string encryption levels to enum values
|
|
456
|
-
encryption_level_map = {
|
|
457
|
-
"Required": _core.EncryptionLevel.REQUIRED,
|
|
458
|
-
"LoginOnly": _core.EncryptionLevel.LOGIN_ONLY,
|
|
459
|
-
"Off": _core.EncryptionLevel.OFF
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
self._config = _core.SslConfig(
|
|
463
|
-
encryption_level=encryption_level_map[encryption_level],
|
|
464
|
-
trust_server_certificate=trust_server_certificate,
|
|
465
|
-
ca_certificate_path=ca_certificate_path,
|
|
466
|
-
enable_sni=enable_sni,
|
|
467
|
-
server_name=server_name
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
@staticmethod
|
|
471
|
-
def development() -> 'SslConfig':
|
|
472
|
-
"""Create SSL config for development (trusts all certificates).
|
|
473
|
-
|
|
474
|
-
Warning: This configuration is insecure and should only be used in development.
|
|
475
|
-
"""
|
|
476
|
-
config = SslConfig.__new__(SslConfig)
|
|
477
|
-
config._config = _core.SslConfig.development()
|
|
478
|
-
return config
|
|
479
|
-
|
|
480
|
-
@staticmethod
|
|
481
|
-
def with_ca_certificate(ca_cert_path: str) -> 'SslConfig':
|
|
482
|
-
"""Create SSL config for production with custom CA certificate.
|
|
483
|
-
|
|
484
|
-
Args:
|
|
485
|
-
ca_cert_path: Path to CA certificate file (.pem, .crt, or .der)
|
|
486
|
-
"""
|
|
487
|
-
config = SslConfig.__new__(SslConfig)
|
|
488
|
-
config._config = _core.SslConfig.with_ca_certificate(ca_cert_path)
|
|
489
|
-
return config
|
|
490
|
-
|
|
491
|
-
@staticmethod
|
|
492
|
-
def login_only() -> 'SslConfig':
|
|
493
|
-
"""Create SSL config that only encrypts login (legacy mode)."""
|
|
494
|
-
config = SslConfig.__new__(SslConfig)
|
|
495
|
-
config._config = _core.SslConfig.login_only()
|
|
496
|
-
return config
|
|
497
|
-
|
|
498
|
-
@staticmethod
|
|
499
|
-
def disabled() -> 'SslConfig':
|
|
500
|
-
"""Create SSL config with no encryption (not recommended)."""
|
|
501
|
-
config = SslConfig.__new__(SslConfig)
|
|
502
|
-
config._config = _core.SslConfig.disabled()
|
|
503
|
-
return config
|
|
504
|
-
|
|
505
|
-
@property
|
|
506
|
-
def encryption_level(self) -> str:
|
|
507
|
-
"""Get the encryption level."""
|
|
508
|
-
return str(self._config.encryption_level)
|
|
509
|
-
|
|
510
|
-
@property
|
|
511
|
-
def trust_server_certificate(self) -> bool:
|
|
512
|
-
"""Get trust server certificate setting."""
|
|
513
|
-
return self._config.trust_server_certificate
|
|
514
|
-
|
|
515
|
-
@property
|
|
516
|
-
def ca_certificate_path(self) -> Optional[str]:
|
|
517
|
-
"""Get CA certificate path."""
|
|
518
|
-
return self._config.ca_certificate_path
|
|
519
|
-
|
|
520
|
-
@property
|
|
521
|
-
def enable_sni(self) -> bool:
|
|
522
|
-
"""Get SNI setting."""
|
|
523
|
-
return self._config.enable_sni
|
|
524
|
-
|
|
525
|
-
@property
|
|
526
|
-
def server_name(self) -> Optional[str]:
|
|
527
|
-
"""Get custom server name."""
|
|
528
|
-
return self._config.server_name
|
|
529
|
-
|
|
530
|
-
def __repr__(self) -> str:
|
|
531
|
-
return f"SslConfig(encryption={self.encryption_level}, trust_cert={self.trust_server_certificate})"
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
class Connection:
|
|
535
|
-
"""Async connection to Microsoft SQL Server database with enhanced type support."""
|
|
536
|
-
|
|
537
|
-
def __init__(
|
|
538
|
-
self,
|
|
539
|
-
connection_string: Optional[str] = None,
|
|
540
|
-
pool_config: Optional[PoolConfig] = None,
|
|
541
|
-
ssl_config: Optional['SslConfig'] = None,
|
|
542
|
-
auto_connect: bool = False,
|
|
543
|
-
server: Optional[str] = None,
|
|
544
|
-
database: Optional[str] = None,
|
|
545
|
-
username: Optional[str] = None,
|
|
546
|
-
password: Optional[str] = None,
|
|
547
|
-
trusted_connection: Optional[bool] = None
|
|
548
|
-
):
|
|
549
|
-
"""Initialize a new async connection.
|
|
550
|
-
|
|
551
|
-
Args:
|
|
552
|
-
connection_string: SQL Server connection string (if not using individual parameters)
|
|
553
|
-
pool_config: Optional connection pool configuration
|
|
554
|
-
ssl_config: Optional SSL/TLS configuration
|
|
555
|
-
auto_connect: If True, automatically connect on creation (not supported for async)
|
|
556
|
-
server: Database server hostname or IP address
|
|
557
|
-
database: Database name to connect to
|
|
558
|
-
username: Username for SQL Server authentication
|
|
559
|
-
password: Password for SQL Server authentication
|
|
560
|
-
trusted_connection: Use Windows integrated authentication (default: True if no username provided)
|
|
561
|
-
|
|
562
|
-
Note:
|
|
563
|
-
Either connection_string OR server must be provided.
|
|
564
|
-
If using individual parameters, server is required.
|
|
565
|
-
If username is provided, password should also be provided for SQL authentication.
|
|
566
|
-
If username is not provided, Windows integrated authentication will be used.
|
|
567
|
-
"""
|
|
568
|
-
py_pool_config = pool_config._config if pool_config else None
|
|
569
|
-
py_ssl_config = ssl_config._config if ssl_config else None
|
|
570
|
-
self._conn = _core.Connection(
|
|
571
|
-
connection_string,
|
|
572
|
-
py_pool_config,
|
|
573
|
-
py_ssl_config,
|
|
574
|
-
server,
|
|
575
|
-
database,
|
|
576
|
-
username,
|
|
577
|
-
password,
|
|
578
|
-
trusted_connection
|
|
579
|
-
)
|
|
580
|
-
self._connected = False
|
|
581
|
-
if auto_connect:
|
|
582
|
-
# Note: Can't await in __init__, so auto_connect won't work for async
|
|
583
|
-
pass
|
|
584
|
-
|
|
585
|
-
async def connect(self) -> None:
|
|
586
|
-
"""Connect to the database asynchronously."""
|
|
587
|
-
await self._conn.connect()
|
|
588
|
-
self._connected = True
|
|
589
|
-
|
|
590
|
-
async def disconnect(self) -> None:
|
|
591
|
-
"""Disconnect from the database asynchronously."""
|
|
592
|
-
await self._conn.disconnect()
|
|
593
|
-
self._connected = False
|
|
594
|
-
|
|
595
|
-
async def is_connected(self) -> bool:
|
|
596
|
-
"""Check if connected to the database."""
|
|
597
|
-
return await self._conn.is_connected()
|
|
598
|
-
|
|
599
|
-
async def execute(self, sql: str, parameters: Optional[Union[List[Any], Parameters, Iterable[Any]]] = None) -> ExecutionResult:
|
|
600
|
-
"""Execute a query asynchronously and return enhanced results.
|
|
601
|
-
|
|
602
|
-
Args:
|
|
603
|
-
sql: SQL query to execute (must be non-empty)
|
|
604
|
-
parameters: Optional parameters - can be:
|
|
605
|
-
- List of values for @P1 placeholders
|
|
606
|
-
- Parameters object for more control
|
|
607
|
-
- Any iterable of values (tuple, set, generator, etc.)
|
|
608
|
-
|
|
609
|
-
Returns:
|
|
610
|
-
ExecutionResult object with rows or affected row count
|
|
611
|
-
|
|
612
|
-
Raises:
|
|
613
|
-
RuntimeError: If not connected to database
|
|
614
|
-
ValueError: If sql is empty or None
|
|
615
|
-
|
|
616
|
-
Examples:
|
|
617
|
-
# Simple list of parameters
|
|
618
|
-
result = await conn.execute("SELECT * FROM users WHERE age > @P1 AND name = @P2", [18, "John"])
|
|
619
|
-
|
|
620
|
-
# Using tuple
|
|
621
|
-
result = await conn.execute("SELECT * FROM users WHERE age > @P1 AND name = @P2", (18, "John"))
|
|
622
|
-
|
|
623
|
-
# Automatic IN clause expansion (handled by Rust for performance)
|
|
624
|
-
result = await conn.execute("SELECT * FROM users WHERE id IN (@P1)", [[1, 2, 3, 4]])
|
|
625
|
-
# Rust automatically expands to: WHERE id IN (@P1, @P2, @P3, @P4)
|
|
626
|
-
|
|
627
|
-
# Using Parameters with automatic IN clause expansion
|
|
628
|
-
params = Parameters([1, 2, 3, 4], "John")
|
|
629
|
-
result = await conn.execute("SELECT * FROM users WHERE id IN (@P1) AND name = @P2", params)
|
|
630
|
-
# Rust expands the list automatically
|
|
631
|
-
|
|
632
|
-
# Using Parameters with type hints
|
|
633
|
-
params = Parameters(Parameter([1, 2, 3, 4], "INT"), "John")
|
|
634
|
-
result = await conn.execute("SELECT * FROM users WHERE id IN (@P1) AND name = @P2", params)
|
|
635
|
-
|
|
636
|
-
# Method chaining with iterables
|
|
637
|
-
params = Parameters().add(18, "INT").add(["admin", "user"])
|
|
638
|
-
result = await conn.execute("SELECT * FROM users WHERE age > @P1 AND role IN (@P2)", params)
|
|
639
|
-
"""
|
|
640
|
-
if not sql or not sql.strip():
|
|
641
|
-
raise ValueError("SQL query cannot be empty or None")
|
|
642
|
-
|
|
643
|
-
if not self._connected:
|
|
644
|
-
raise RuntimeError("Not connected to database. Call await conn.connect() first.")
|
|
645
|
-
|
|
646
|
-
if parameters is None:
|
|
647
|
-
py_result = await self._conn.execute(sql)
|
|
648
|
-
elif isinstance(parameters, Parameters):
|
|
649
|
-
# Convert Parameters object to list of values
|
|
650
|
-
param_values = parameters.to_list()
|
|
651
|
-
py_result = await self._conn.execute_with_python_params(sql, param_values)
|
|
652
|
-
elif hasattr(parameters, '__iter__') and not isinstance(parameters, (str, bytes)):
|
|
653
|
-
# Handle any iterable (list, tuple, set, generator, etc.)
|
|
654
|
-
# Convert to list to ensure we can pass it to the Rust layer
|
|
655
|
-
param_values = list(parameters)
|
|
656
|
-
py_result = await self._conn.execute_with_python_params(sql, param_values)
|
|
657
|
-
else:
|
|
658
|
-
# Single value - wrap in list
|
|
659
|
-
py_result = await self._conn.execute_with_python_params(sql, [parameters])
|
|
660
|
-
|
|
661
|
-
return ExecutionResult(py_result)
|
|
662
|
-
|
|
663
|
-
async def __aenter__(self):
|
|
664
|
-
"""Async context manager entry."""
|
|
665
|
-
await self.connect()
|
|
666
|
-
return self
|
|
667
|
-
|
|
668
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
669
|
-
"""Async context manager exit."""
|
|
670
|
-
await self.disconnect()
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
def version() -> str:
|
|
674
|
-
"""Get the version of the mssql-python-rust library.
|
|
675
|
-
|
|
676
|
-
Returns:
|
|
677
|
-
Version string
|
|
678
|
-
"""
|
|
679
|
-
return _core.version()
|
|
680
|
-
|
|
681
|
-
# Re-export core types for direct access if needed
|
|
682
|
-
RustConnection = _core.Connection # Rename to avoid conflict with our main connect() function
|
|
683
|
-
RustQuery = _core.Query # Rename to avoid conflict with our wrapper
|
|
684
|
-
PyRow = _core.Row
|
|
685
|
-
PyValue = _core.Value
|
|
686
|
-
PyExecutionResult = _core.ExecutionResult
|
|
687
|
-
PyQuery = _core.Query
|
|
688
|
-
|
|
689
|
-
# Export main API
|
|
690
|
-
__all__ = [
|
|
691
|
-
'Connection', # Main connection class
|
|
692
|
-
'Parameter', # Individual parameter with optional type
|
|
693
|
-
'Parameters', # Parameter container for execute()
|
|
694
|
-
'Row',
|
|
695
|
-
'ExecutionResult',
|
|
696
|
-
'PoolConfig',
|
|
697
|
-
'SslConfig', # SSL/TLS configuration
|
|
698
|
-
'version', # Version function
|
|
699
|
-
# Core types for advanced usage
|
|
700
|
-
'RustConnection',
|
|
701
|
-
'RustQuery',
|
|
702
|
-
'PyRow',
|
|
703
|
-
'PyValue',
|
|
704
|
-
'PyExecutionResult',
|
|
705
|
-
'PyQuery'
|
|
706
|
-
]
|