Skip to content

[Draft] Async Query execution using RUST - POC#681

Draft
subrata-ms wants to merge 6 commits into
mainfrom
subrata-ms/AsynQueryPyCore
Draft

[Draft] Async Query execution using RUST - POC#681
subrata-ms wants to merge 6 commits into
mainfrom
subrata-ms/AsynQueryPyCore

Conversation

@subrata-ms

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#<WORK_ITEM_ID>

GitHub Issue: #<ISSUE_NUMBER>


Summary

Adds mssql_python/aio.py exposing AsyncConnection, AsyncCursor and the
awaitable factory connect_async. Thin wrappers over the Rust
mssql_py_core.PyCoreAsyncConnection / PyCoreAsyncCursor.

- Every awaitable method is defined as 'async def foo(...): return await
  self._core.foo_async(...)' so callers can use asyncio.create_task /
  gather / wait_for on them. Rust future_into_py returns an awaitable
  (not a coroutine); create_task() would otherwise TypeError.
- AsyncCursor.execute takes (operation, timeout_sec=30) and returns self.
- AsyncCursor.fetchone returns tuple or None.
- AsyncCursor.cancel is sync and safe to call from another task.
- AsyncConnection.connect classmethod + module-level connect_async factory
  keep the async idiom even though connect is currently sync under the hood.
- Both classes support async context managers (__aenter__ / __aexit__) and
  expose .closed. Idempotent close().
- Use-after-close raises InterfaceError with a clean driver/DDBC message.

Wired up in mssql_python/__init__.py so users get
'mssql_python.connect_async', 'mssql_python.AsyncConnection', and
'mssql_python.AsyncCursor' via the top-level package.

End-to-end wrapper smoke against local SQL Server 2022 container:
- basic: SELECT 42, multi-col decode, exhaustion, re-execute
- non-blocking: 60 ticks during 400 ms WAITFOR (elapsed 0.412s)
- concurrent: 2x500ms queries via asyncio.gather in 0.542s
- cancel: WAITFOR '00:00:10' cancelled via asyncio.create_task, exception
  in 3 ms
- after-close: execute/fetchone raise InterfaceError

Regression: tests/test_019_bulkcopy.py still 10/10 passing.
- pytest.ini: register the 'asyncio' marker and set asyncio_mode=strict so
  each async test opts in explicitly (via module-level pytestmark). This
  leaves the 678-test sync suite unchanged.
- tests/conftest.py: add async fixtures (async_conn_str session-scoped,
  async_connection + async_cursor function-scoped) inline so pytest
  auto-loads them; skip the async suite if DB_CONNECTION_STRING is unset.
  Function scope keeps cancellation / close tests independent.
- tests/test_030_async_poc.py: 13 tests across 4 classes exercising the
  full POC surface end-to-end:
    * TestAsyncBasics (6): literal, multi-col, exhaustion, multi-row,
      re-execute, and async context-manager idiom via connect_async.
    * TestAsyncErrors (4): bad SQL, execute/fetchone after cursor close,
      cursor() after connection close.
    * TestAsyncNonBlocking (2): ticker keeps ticking during a 400 ms
      WAITFOR (proves loop non-blocking); two connections × 500 ms in
      < 0.9 s via asyncio.gather (proves multi-thread runtime).
    * TestAsyncCancel (1): asyncio.create_task + cur.cancel() aborts a
      10 s WAITFOR in < 5 s.

Results:
- tests/test_030_async_poc.py: 13 passed in 1.54 s.
- Regression tests/test_019_bulkcopy.py + tests/test_003_connection.py +
  tests/test_004_cursor.py: 678 passed / 5 skipped / 4 warnings in 36 s.
- Combined pytest tests/test_019_bulkcopy.py tests/test_030_async_poc.py:
  23 passed in 1.99 s (async + sync cohabit cleanly).

Note: the earlier attempt at 'pytest_plugins = tests.conftest_async' failed
because tests/ has no __init__.py. Folded fixtures into tests/conftest.py
directly — simplest and idiomatic.
Keeps ASYNC_POC_SPEC.md / ASYNC_POC_RUNBOOK.md as personal working notes
without committing them upstream. History for the docs commit (previously
9fe5fe6) has been rebase-dropped from this branch.
@subrata-ms subrata-ms force-pushed the subrata-ms/AsynQueryPyCore branch from 5b1ebb4 to f9eab1e Compare July 15, 2026 11:17
Mirrors the sync pair (cursor.py / connection.py) — one class per file,
keeping module names discoverable by their responsibility.

- mssql_python/cursor_async.py       AsyncCursor
- mssql_python/connection_async.py   AsyncConnection + connect_async factory
- mssql_python/aio.py                deleted

Public surface unchanged: mssql_python.AsyncConnection, AsyncCursor and
connect_async are still re-exported from the package __init__ and remain
the recommended entry points for user code.

Verified: 23/23 async + bulkcopy tests pass after the split.
Two tests in a new TestAsyncScale class exercise the pyo3-async-runtimes
+ Tokio pipeline under real concurrent load:

- test_hundred_connections_correctness: 100 tasks, each opening a fresh
  AsyncConnection, running SELECT n, fetchone, and closing. Verifies
  every value round-trips and the pipeline handles 100 concurrent
  sessions from a single Python process.

- test_hundred_connections_true_parallelism: 100 tasks each running a
  200 ms server-side WAITFOR. Asserts wall time is well below the 20 s
  serial baseline, proving the multi-thread Tokio runtime + non-blocking
  future_into_py futures actually run in parallel.

Both marked @pytest.mark.slow (currently included by default; can be
excluded via pytest -m 'not slow' if a runner is resource-constrained).

Measurements against local SQL Server 2022 container:
- 100 connect+select+close cycles:     0.963 s  (~10 ms/conn amortised)
- 100 x 200 ms WAITFOR:                 1.107 s  (~18x over serial baseline)

Combined pytest tests/test_030_async_poc.py tests/test_019_bulkcopy.py:
25 passed in 3.51 s.
…query

Replaces the trivial SELECT / WAITFOR stubs with a T-SQL batch that is
both non-trivial and predictably verifiable:

  WAITFOR DELAY ...             -- keeps it long-running
  WITH nums AS ( recursive CTE producing 50 rows )
  SELECT
    task_id  INT,               -- interpolated identity
    n        INT,               -- row order 1..50
    square   INT      (n*n),    -- integer arithmetic
    label    NVARCHAR,          -- per-row string with task_id + n
    root     FLOAT (SQRT(n))    -- floating-point decode
  FROM nums
  OPTION (MAXRECURSION 50)

Every value is server-computed, so any driver-side corruption (wrong
column order, truncated string, wrong float bits, wrong integer width)
surfaces as an assertion failure. Row-by-row validation across 5 000
rows per test proves the streaming fetchone_async decode is correct at
scale, not just for a single sentinel value.

Helpers extracted for readability:
- _complex_long_running_query(task_id, wait_ms) — batch builder.
- _verify_task_rows(task_id, rows) — deterministic per-row assertions.

Tests updated:
- test_hundred_connections_complex_query_correctness  (100 ms wait)
- test_hundred_connections_long_running_parallelism   (200 ms wait)

Measurements against local SQL Server 2022 container:
- Correctness (5 000 rows verified, 100 ms wait/task): 1.782 s @ 2 806 r/s
- Parallelism (5 000 rows verified, 200 ms wait/task): 1.791 s @ 2 791 r/s
  (serial baseline ~20 s just for the WAITFORs)

Combined pytest tests/test_030_async_poc.py tests/test_019_bulkcopy.py:
25 passed in 5.17 s.
@subrata-ms subrata-ms changed the title Async Query execution using RUST - POC [Draft] Async Query execution using RUST - POC Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant