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/aio.py ADDED
@@ -0,0 +1,533 @@
1
+ """Native asyncio API on the Rust core.
2
+
3
+ from arrowsql import aio
4
+
5
+ async with await aio.connect(conn_str, autocommit=True) as conn:
6
+ cur = conn.cursor()
7
+ await cur.execute("SELECT id, name FROM t WHERE id > ?", 10)
8
+ rows = await cur.fetchall()
9
+
10
+ pool = await aio.create_pool(conn_str, min_size=2, max_size=16)
11
+ rows = await pool.fetch("SELECT * FROM t WHERE id = ?", 42)
12
+
13
+ Every operation runs on the Rust core's tokio runtime with the GIL released;
14
+ results come back to the event loop through one completion hub per loop.
15
+ Cancelling the awaiting task (``task.cancel()``, ``asyncio.wait_for``
16
+ timeouts) cancels the query on the server too. See docs/ASYNC.md.
17
+
18
+ Unix only (the hub needs ``loop.add_reader``): on other platforms the module
19
+ imports, but ``connect`` and ``create_pool`` raise ``NotSupportedError``.
20
+ """
21
+
22
+ import asyncio
23
+ import weakref
24
+ from typing import Any, Optional, Sequence
25
+
26
+ from arrowsql import ddbc_bindings as _core
27
+ from arrowsql.constants import ConstantsDDBC as _const
28
+ from arrowsql.cursor import Cursor as _SyncCursor
29
+ from arrowsql.exceptions import InterfaceError, NotSupportedError, ProgrammingError
30
+ from arrowsql.helpers import get_settings
31
+
32
+ __all__ = ["connect", "create_pool", "AsyncConnection", "AsyncCursor", "Pool"]
33
+
34
+ _hubs: "weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, Any]" = weakref.WeakKeyDictionary()
35
+ _SQL_NULLABLE = _const.SQL_NULLABLE.value
36
+ _map_type = _SyncCursor._map_data_type
37
+
38
+
39
+ class _Shape:
40
+ """Description and column maps of one result shape (cached by the hub)."""
41
+
42
+ __slots__ = ("description", "column_map", "column_map_lower", "column_names")
43
+
44
+ def __init__(self, columns):
45
+ lowercase = get_settings().lowercase
46
+ description = []
47
+ names = []
48
+ for name, sql_type, size, digits, nullable in columns:
49
+ if lowercase:
50
+ name = name.lower()
51
+ names.append(name)
52
+ description.append(
53
+ (name, _map_type(None, sql_type), None, size, size, digits, nullable == _SQL_NULLABLE)
54
+ )
55
+ self.description = description
56
+ self.column_names = tuple(names)
57
+ self.column_map = {name: i for i, name in enumerate(names)}
58
+ self.column_map_lower = (
59
+ None if lowercase else {name.lower(): i for i, name in enumerate(names)}
60
+ )
61
+
62
+
63
+ def _require_native() -> None:
64
+ # RUST-PORT: the native hub is compiled for Unix only (src/lib.rs).
65
+ if not hasattr(_core, "AioHub"):
66
+ message = "arrowsql.aio is only available on Unix platforms (Linux, macOS)"
67
+ raise NotSupportedError(driver_error=message, ddbc_error=message)
68
+
69
+
70
+ def _hub():
71
+ _require_native()
72
+ loop = asyncio.get_running_loop()
73
+ hub = _hubs.get(loop)
74
+ if hub is None:
75
+ # The hub holds the loop weakly: the loop owns the hub through add_reader.
76
+ hub = _core.AioHub(weakref.ref(loop), _Shape)
77
+ loop.add_reader(hub.fileno(), hub.drain)
78
+ _hubs[loop] = hub
79
+ return hub
80
+
81
+
82
+ async def _wait(conn, op):
83
+ """Await `op` = (future, request id); cancel the request on cancellation."""
84
+ future, request = op
85
+ try:
86
+ return await future
87
+ except asyncio.CancelledError:
88
+ conn.cancel(request)
89
+ raise
90
+
91
+
92
+ def _params(params: Sequence[Any]):
93
+ """execute(sql, a, b) and execute(sql, (a, b)) both work."""
94
+ if len(params) == 1 and isinstance(params[0], (list, tuple)):
95
+ return params[0]
96
+ return params
97
+
98
+
99
+ class AsyncCursor:
100
+ """DB-API style cursor; every method that talks to the server is a coroutine."""
101
+
102
+ __slots__ = (
103
+ "_conn", "_c", "_id", "_batch", "_rows", "_pos", "_done",
104
+ "description", "rowcount", "messages", "arraysize", "_closed",
105
+ )
106
+
107
+ def __init__(self, connection: "AsyncConnection"):
108
+ self._conn = connection
109
+ self._c = connection._c
110
+ self._id = self._c.cursor()
111
+ self._batch = None # rows read ahead by execute (converted lazily)
112
+ self._rows = [] # rows fetched by fetchone in chunks
113
+ self._pos = 0
114
+ self._done = True
115
+ self.description = None
116
+ self.rowcount = -1
117
+ self.messages = []
118
+ self.arraysize = 1
119
+ self._closed = False
120
+
121
+ @property
122
+ def connection(self) -> "AsyncConnection":
123
+ return self._conn
124
+
125
+ def _install(self, result) -> None:
126
+ shape, batch, done, rowcount, messages = result
127
+ self._batch = batch
128
+ self._rows = []
129
+ self._pos = 0
130
+ self._done = done
131
+ self.description = shape.description if shape is not None else None
132
+ self.rowcount = rowcount
133
+ self.messages = messages or []
134
+
135
+ def _check(self) -> None:
136
+ if self._closed:
137
+ raise _interface_error("Cursor is closed")
138
+
139
+ async def execute(self, operation: str, *parameters: Any) -> "AsyncCursor":
140
+ self._check()
141
+ params = _params(parameters) if parameters else None
142
+ result = await _wait(self._c, self._c.execute(_hub(), self._id, operation, params))
143
+ self._install(result)
144
+ return self
145
+
146
+ async def executemany(self, operation: str, seq_of_parameters: Sequence[Sequence[Any]]) -> None:
147
+ self._check()
148
+ rows = await _wait(self._c, self._c.executemany(_hub(), self._id, operation, seq_of_parameters))
149
+ self._batch = None
150
+ self._done = True
151
+ self.description = None
152
+ self.rowcount = rows
153
+
154
+ async def _more(self, n: int) -> list:
155
+ rows, done = await _wait(self._c, self._c.fetch(_hub(), self._id, n))
156
+ self._done = done
157
+ return rows
158
+
159
+ def _local(self, n: int) -> list:
160
+ """Up to `n` rows already on the Python side (no await)."""
161
+ out = []
162
+ batch = self._batch
163
+ if batch is not None and batch.remaining():
164
+ out = batch.take(n)
165
+ if len(out) < n and self._pos < len(self._rows):
166
+ end = self._pos + (n - len(out))
167
+ if out:
168
+ out.extend(self._rows[self._pos:end])
169
+ else:
170
+ out = self._rows[self._pos:end]
171
+ self._pos = min(end, len(self._rows))
172
+ return out
173
+
174
+ async def fetchone(self) -> Optional[Any]:
175
+ batch = self._batch
176
+ if batch is not None:
177
+ row = batch.next()
178
+ if row is not None:
179
+ return row
180
+ self._batch = None
181
+ if self._pos < len(self._rows):
182
+ row = self._rows[self._pos]
183
+ self._pos += 1
184
+ return row
185
+ if self._done:
186
+ return None
187
+ self._rows = await self._more(max(self.arraysize, 512))
188
+ self._pos = 0
189
+ return await self.fetchone()
190
+
191
+ async def fetchmany(self, size: Optional[int] = None) -> list:
192
+ size = self.arraysize if size is None else size
193
+ out = self._local(size)
194
+ while len(out) < size and not self._done:
195
+ more = await self._more(size - len(out))
196
+ if out:
197
+ out.extend(more)
198
+ else:
199
+ out = more
200
+ return out
201
+
202
+ async def fetchall(self) -> list:
203
+ out = self._local(2**62)
204
+ while not self._done: # a result that fails after some rows ends with the error
205
+ more = await self._more(2**62)
206
+ if out:
207
+ out.extend(more)
208
+ else:
209
+ out = more
210
+ return out
211
+
212
+ async def fetchall_arrow(self, batch_size: int = 65_536):
213
+ """All remaining rows as a native ArrowTable (DuckDB/polars/pyarrow read it directly)."""
214
+ self._check()
215
+ if self.description is None:
216
+ raise _programming_error("No active result set. Cannot fetch Arrow batch.")
217
+ if self._pos < len(self._rows):
218
+ raise _programming_error("fetchall_arrow() after a partial fetchone() is not supported")
219
+ head, self._batch = self._batch, None
220
+ table = await _wait(
221
+ self._c, self._c.fetch_arrow(_hub(), self._id, head, batch_size)
222
+ )
223
+ self._done = True
224
+ return table
225
+
226
+ async def nextset(self) -> Optional[bool]:
227
+ self._check()
228
+ result = await _wait(self._c, self._c.nextset(_hub(), self._id))
229
+ if result is None:
230
+ self._batch = None
231
+ self._done = True
232
+ self.description = None
233
+ return None
234
+ self._install(result)
235
+ return True
236
+
237
+ async def close(self) -> None:
238
+ if not self._closed:
239
+ self._closed = True
240
+ self._batch = None
241
+ self._c.close_cursor(self._id)
242
+
243
+ def __del__(self):
244
+ # Rust keeps a cursor's results until it is closed: retire it with the object.
245
+ try:
246
+ if not self._closed:
247
+ self._closed = True
248
+ self._c.close_cursor(self._id)
249
+ except Exception:
250
+ pass
251
+
252
+ def __aiter__(self):
253
+ return self
254
+
255
+ async def __anext__(self):
256
+ batch = self._batch
257
+ if batch is not None:
258
+ row = batch.next()
259
+ if row is not None:
260
+ return row
261
+ elif self._pos < len(self._rows):
262
+ row = self._rows[self._pos]
263
+ self._pos += 1
264
+ return row
265
+ row = await self.fetchone()
266
+ if row is None:
267
+ raise StopAsyncIteration
268
+ return row
269
+
270
+ async def __aenter__(self) -> "AsyncCursor":
271
+ return self
272
+
273
+ async def __aexit__(self, *exc) -> None:
274
+ await self.close()
275
+
276
+
277
+ class _FetchMixin:
278
+ """fetch/fetchrow/fetchval shared by connections and pools; needs ``_query``."""
279
+
280
+ __slots__ = ()
281
+
282
+ async def fetch(self, operation: str, *parameters: Any) -> list:
283
+ """All rows of the first result, in one round trip."""
284
+ return await self._query(operation, parameters, 0)
285
+
286
+ async def fetchrow(self, operation: str, *parameters: Any) -> Optional[Any]:
287
+ return await self._query(operation, parameters, 1)
288
+
289
+ async def fetchval(self, operation: str, *parameters: Any) -> Any:
290
+ return await self._query(operation, parameters, 2)
291
+
292
+
293
+ class AsyncConnection(_FetchMixin):
294
+ """One TDS session. Operations on one connection run one at a time."""
295
+
296
+ def __init__(self, native):
297
+ self._c = native
298
+ self._closed = False
299
+
300
+ @property
301
+ def autocommit(self) -> bool:
302
+ return self._c.autocommit
303
+
304
+ async def set_autocommit(self, value: bool) -> None:
305
+ await _wait(self._c, self._c.set_autocommit(_hub(), bool(value)))
306
+
307
+ @property
308
+ def timeout(self) -> int:
309
+ return self._c.timeout
310
+
311
+ @timeout.setter
312
+ def timeout(self, value: int) -> None:
313
+ self._c.timeout = value
314
+
315
+ @property
316
+ def closed(self) -> bool:
317
+ return self._closed
318
+
319
+ def cursor(self) -> AsyncCursor:
320
+ if self._closed:
321
+ raise _interface_error("Connection is closed")
322
+ return AsyncCursor(self)
323
+
324
+ async def execute(self, operation: str, *parameters: Any) -> AsyncCursor:
325
+ return await self.cursor().execute(operation, *parameters)
326
+
327
+ async def _query(self, operation, parameters, mode):
328
+ if self._closed:
329
+ raise _interface_error("Connection is closed")
330
+ params = _params(parameters) if parameters else None
331
+ return await _wait(self._c, self._c.query(_hub(), operation, params, mode))
332
+
333
+ async def commit(self) -> None:
334
+ await _wait(self._c, self._c.commit(_hub()))
335
+
336
+ async def rollback(self) -> None:
337
+ await _wait(self._c, self._c.rollback(_hub()))
338
+
339
+ async def close(self) -> None:
340
+ if not self._closed:
341
+ self._closed = True
342
+ await _wait(self._c, self._c.close(_hub()))
343
+
344
+ async def __aenter__(self) -> "AsyncConnection":
345
+ return self
346
+
347
+ async def __aexit__(self, exc_type, exc, tb) -> None:
348
+ try:
349
+ if not self._closed and not self.autocommit:
350
+ if exc_type is None:
351
+ await self.commit()
352
+ else:
353
+ await self.rollback()
354
+ finally:
355
+ await self.close()
356
+
357
+
358
+ _POOL_TIMEOUT_TEXT = "Timed out waiting for a connection from the pool"
359
+
360
+
361
+ async def _pool_wait(pool, op):
362
+ """`_wait` that raises asyncio.TimeoutError when the pool's acquire timeout expires."""
363
+ try:
364
+ return await _wait(pool, op)
365
+ except Exception as exc:
366
+ if _POOL_TIMEOUT_TEXT in str(exc):
367
+ raise asyncio.TimeoutError(_POOL_TIMEOUT_TEXT) from exc
368
+ raise
369
+
370
+
371
+ class _Acquire:
372
+ def __init__(self, pool: "Pool", timeout: Optional[float] = None):
373
+ self._pool = pool
374
+ self._timeout = timeout
375
+ self._conn: Optional[AsyncConnection] = None
376
+
377
+ def __await__(self):
378
+ return self._acquire().__await__()
379
+
380
+ async def _acquire(self) -> AsyncConnection:
381
+ native = await _pool_wait(
382
+ self._pool._p, self._pool._p.acquire(_hub(), self._timeout)
383
+ )
384
+ return AsyncConnection(native)
385
+
386
+ async def __aenter__(self) -> AsyncConnection:
387
+ self._conn = await self._acquire()
388
+ return self._conn
389
+
390
+ async def __aexit__(self, exc_type, exc, tb) -> None:
391
+ # Same as leaving ``async with connect(...)``: a pool created with
392
+ # autocommit=False commits on a clean exit and rolls back on an
393
+ # exception, then the session goes back to the pool.
394
+ conn, self._conn = self._conn, None
395
+ if conn is not None:
396
+ await conn.__aexit__(exc_type, exc, tb)
397
+
398
+
399
+ class Pool(_FetchMixin):
400
+ """A pool of sessions; the fetch/execute shortcuts use one session per call.
401
+
402
+ With ``autocommit=False``, leaving ``async with pool.acquire()`` commits on
403
+ a clean exit and rolls back on an exception (like ``AsyncConnection``);
404
+ closing a connection obtained with ``await pool.acquire()`` rolls back.
405
+ """
406
+
407
+ def __init__(self, native):
408
+ self._p = native
409
+
410
+ def acquire(self, *, timeout: Optional[float] = None) -> _Acquire:
411
+ """``async with pool.acquire() as conn`` (or ``conn = await pool.acquire()``).
412
+
413
+ ``timeout`` (seconds, default: the pool's ``acquire_timeout``) bounds the
414
+ wait for a free session, including opening a new one; on expiry
415
+ ``asyncio.TimeoutError`` is raised."""
416
+ return _Acquire(self, timeout)
417
+
418
+ @property
419
+ def acquire_timeout(self) -> float:
420
+ """Default seconds ``acquire`` and the fetch/execute shortcuts wait (0 = unbounded)."""
421
+ return self._p.acquire_timeout
422
+
423
+ @acquire_timeout.setter
424
+ def acquire_timeout(self, value: float) -> None:
425
+ self._p.acquire_timeout = value
426
+
427
+ async def execute(self, operation: str, *parameters: Any) -> int:
428
+ """Run a statement; returns its rowcount (or rows, if it returns any)."""
429
+ return await self._query(operation, parameters, 0)
430
+
431
+ async def _query(self, operation, parameters, mode):
432
+ params = _params(parameters) if parameters else None
433
+ return await _pool_wait(self._p, self._p.query(_hub(), operation, params, mode))
434
+
435
+ async def close(self) -> None:
436
+ await _wait(self._p, self._p.close(_hub()))
437
+
438
+ @property
439
+ def max_size(self) -> int:
440
+ return self._p.max_size
441
+
442
+ @property
443
+ def idle(self) -> int:
444
+ return self._p.idle
445
+
446
+ @property
447
+ def in_use(self) -> int:
448
+ return self._p.in_use
449
+
450
+ async def __aenter__(self) -> "Pool":
451
+ return self
452
+
453
+ async def __aexit__(self, *exc) -> None:
454
+ await self.close()
455
+
456
+
457
+ async def connect(
458
+ connection_str: str,
459
+ autocommit: bool = False,
460
+ timeout: int = 0,
461
+ *,
462
+ tuple_rows: bool = False,
463
+ ) -> AsyncConnection:
464
+ """Open a connection. ``timeout`` is the per-query timeout in seconds
465
+ (0 = none); ``tuple_rows=True`` returns plain tuples instead of Row objects.
466
+
467
+ Cancelling the awaiting task (``asyncio.wait_for``) abandons the login."""
468
+ hub = _hub() # first: it reports an unsupported platform before the native symbol is looked up
469
+ future, request, opening = _core.aio_connect(
470
+ hub, connection_str, autocommit, timeout, tuple_rows
471
+ )
472
+ native = await _wait(opening, (future, request))
473
+ return AsyncConnection(native)
474
+
475
+
476
+ async def create_pool(
477
+ connection_str: str,
478
+ min_size: int = 1,
479
+ max_size: int = 10,
480
+ autocommit: bool = True,
481
+ timeout: int = 0,
482
+ *,
483
+ tuple_rows: bool = False,
484
+ reset: bool = True,
485
+ max_inactive_connection_lifetime: float = 300.0,
486
+ max_lifetime: float = 0.0,
487
+ acquire_timeout: float = 0.0,
488
+ validate_after: float = 30.0,
489
+ ) -> Pool:
490
+ """Open ``min_size`` sessions now; up to ``max_size`` on demand.
491
+
492
+ ``reset=True`` resets each session's state (temp tables, SET options,
493
+ open transactions are rolled back regardless) before it is reused, via the
494
+ TDS reset flag on the next request; ``reset=False`` skips the server-side
495
+ reset for pools whose queries leave no session state behind.
496
+
497
+ Lifetimes are in seconds, 0 turns a limit off (asyncpg-style):
498
+
499
+ * ``max_inactive_connection_lifetime``: idle sessions older than this are
500
+ closed (checked when sessions are returned or handed out).
501
+ * ``max_lifetime``: sessions older than this are closed when returned and
502
+ never handed out again.
503
+ * ``acquire_timeout``: default wait for a free session in ``pool.acquire()``
504
+ and the shortcuts; ``asyncio.TimeoutError`` on expiry.
505
+ * ``validate_after``: a session idle at least this long is checked with a
506
+ ``SELECT 1`` before it is handed out; a dead one is replaced.
507
+
508
+ Cancelling the awaiting task abandons the sessions still being opened."""
509
+ hub = _hub() # first: it reports an unsupported platform before the native symbol is looked up
510
+ future, request, opening = _core.aio_create_pool(
511
+ hub,
512
+ connection_str,
513
+ min_size,
514
+ max_size,
515
+ autocommit,
516
+ timeout,
517
+ tuple_rows,
518
+ reset,
519
+ max_inactive_connection_lifetime,
520
+ max_lifetime,
521
+ acquire_timeout,
522
+ validate_after,
523
+ )
524
+ native = await _wait(opening, (future, request))
525
+ return Pool(native)
526
+
527
+
528
+ def _interface_error(message: str):
529
+ return InterfaceError(driver_error=message, ddbc_error=message)
530
+
531
+
532
+ def _programming_error(message: str):
533
+ return ProgrammingError(driver_error=message, ddbc_error=message)