Skip to content

Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor) - #205

Draft
Saurabh Singh (saurabh500) wants to merge 1 commit into
dev/saurabh/rewire-odbc-sync-core-l5from
dev/saurabh/rewire-py-core-sync-async-cursor-l6
Draft

Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor)#205
Saurabh Singh (saurabh500) wants to merge 1 commit into
dev/saurabh/rewire-odbc-sync-core-l5from
dev/saurabh/rewire-py-core-sync-async-cursor-l6

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

L6 — Rewire mssql-py-core onto the reactor-free sync TDS core.

Framing (correctness/architecture, not a perf win yet): This layer (and the whole L-stack) is a correctness/architecture refactor — one protocol core with sync and async shells that deletes the per-row block_on tax and the fast::* hack. It is NOT a native-beating performance win yet. VarcharMax/PLP (~818× on both the async-before and sync-after variants, pre-existing), the Decimal/DateTime2 conversion gaps, and the per-column SQLGetData conversion/alloc path are the real performance debt to burn down next. It earns its keep on architecture and correctness, and it sets up the perf work rather than delivering it.

Stacked on the frozen L5 tip (dev/saurabh/rewire-odbc-sync-core-l5 @ 5c09f0e4). Append-only single commit; base = L5 branch, not main. Scope is mssql-py-core only — all frozen crates (mssql-tds, mssql-odbc, mssql-mock-tds, mssql-js, mssql-tds-cli) are empty-diff.

What this does

The Python bindings now share ONE protocol core across two cursor surfaces via a flip-in-place ownership cell:

  • enum PyClient { Async(TdsClient) | Sync(TdsSyncClient) | Transitioning | Dead(String) } behind Arc<std::sync::Mutex<PyClient>> (pyclient.rs). The Arc points at the cell, so Arc::try_unwrap is never used — cursors keep their clone and match the enum under the lock.
  • Flip is std::mem::replace(&mut *guard, Transitioning) → call into_sync/into_async on the moved-out value → store the result variant back. Transitioning is a sentinel that only exists between take and store (no early-return while transitioning); an unrecoverable flip stores Dead(String) and surfaces Err.
  • Runtime-context hard rule: every into_sync() runs inside handle.enter() so the runtime handle is captured for the later revert (a naked into_sync() off-runtime would capture None and poison into_async).
  • Revert-before-control-plane: revert to Async at end-of-rows / before close_query / before any control-plane op, mirroring L5.

Cursor surfaces (mode A for this layer)

  • PyCoreSyncCursor (new): reactor-free row-pull. Plaintext connections flip to the Sync(TdsSyncClient) arm and drive next_row_into with no block_on; TLS/non-eligible transports fall back to the block_on-over-async path, byte-identically.
  • PyCoreCursor (existing): kept exactly as-is — block_on-over-async backed. Public Python API unchanged — no regression. This is not a first-class async cursor: its block_on blocks the Python event loop. The genuine coroutine (async def / awaitable) cursor is L7, not this layer.
  • Connection.cursor() / Connection.sync_cursor() expose the two surfaces.
  • execute / COLMETADATA / advance / close / DML / bulkcopy / auth all stay async (control-plane rule); the sync flip is for the pure SELECT row-pull hot loop only, per result set.

rowcount

Additive rowcount captured from last_rows_affected() pre-flip on the async arm (faithful-count parity contract, populated by the shared apply_row_read_token count_map at L4 B′/L5). No independent count logic.

Scope decision (mode A) — real coroutine async cursor is L7

This layer ships mode A: sync arm reactor-free + the existing cursor retained as block_on-over-async. A genuinely first-class coroutine async cursor (pyo3-async-runtimes / future_into_py driving next_row_into().await on the Async arm, event-loop driven, non-blocking) is a large net-new public surface and lands as its own reviewable layer L7 stacked on top of this one. No pyo3-async-runtimes dependency is added here; Cargo.lock is unchanged.

Build / test path

mssql-py-core is edition 2024 and excluded from the cargo workspace, so the plain cargo b* aliases do NOT cover it — validated via scripts\bfmt.ps1 + scripts\bclippy.ps1 (both cover py-core) + maturin + the Python suite through the existing conftest.py fixtures.

Tests

  • Sync-cursor fetch on a plaintext connection (reactor-free sync path).
  • Sync-cursor TLS-fallback (block_on-over-async path) — env-skips locally on the pre-existing mock-TLS-on-Windows timeout; CI-validated.
  • Attribute-set parity: the new sync cursor and the existing block_on-backed cursor expose the same surface.
  • Byte parity: sync fetchone results identical to the block_on path.
  • Error-mid-fetch sync recovery: mid-stream error reverts via into_async drain, connection reused (validated live: SQL 245 conversion error mid-stream).
  • DML rowcount + multi-row SELECT parity validated against a live localhost SQL Server.

Gates: scripts\bfmt.ps1 clean, scripts\bclippy.ps1 (-D warnings) clean (workspace + py-core), maturin develop builds mssql_py_core + mssql_mock_tds, mock cursor suite = 10 passed / 1 env-skip.

Checklist

  • cargo bfmt passes
  • cargo bclippy passes
  • cargo btest passes
  • New/changed functionality has tests
  • Public API changes are documented

Stack governance: DRAFT — the whole stack flips to ready-for-review together, bottom-to-top, after parent sign-off.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

Related work item / issue: Tracked as part of the sans-I/O native stack (#192)

@saurabh500 Saurabh Singh (saurabh500) changed the title Rewire mssql-py-core onto the reactor-free sync core with sync and async cursors Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor) Aug 10, 2026
@saurabh500
Saurabh Singh (saurabh500) force-pushed the dev/saurabh/rewire-py-core-sync-async-cursor-l6 branch from c08aea3 to d9dba58 Compare August 10, 2026 05:31
@saurabh500 Saurabh Singh (saurabh500) changed the title Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor) Rewire mssql-py-core onto the reactor-free sync core with sync and async cursors Aug 10, 2026
@saurabh500
Saurabh Singh (saurabh500) force-pushed the dev/saurabh/rewire-py-core-sync-async-cursor-l6 branch 2 times, most recently from a0d95c0 to c08aea3 Compare August 10, 2026 05:41
@saurabh500 Saurabh Singh (saurabh500) changed the title Rewire mssql-py-core onto the reactor-free sync core with sync and async cursors Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor) Aug 10, 2026
@saurabh500
Saurabh Singh (saurabh500) force-pushed the dev/saurabh/rewire-py-core-sync-async-cursor-l6 branch 2 times, most recently from a8b9f5a to c08aea3 Compare August 10, 2026 05:56
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as ready for review August 10, 2026 06:38
@saurabh500
Saurabh Singh (saurabh500) requested a review from a team as a code owner August 10, 2026 06:38
Copilot AI balanced review requested due to automatic review settings August 10, 2026 06:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Rewires Python bindings to share the async and reactor-free sync TDS core through an opt-in synchronous cursor.

Changes:

  • Adds the shared async/sync client state machine.
  • Introduces PyCoreSyncCursor and row-count support.
  • Adds live-server and mock-server coverage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mssql-py-core/src/pyclient.rs Implements shared client transitions and operations.
mssql-py-core/src/sync_cursor.rs Adds the reactor-free sync cursor.
mssql-py-core/src/cursor.rs Adopts the shared client and exposes rowcount.
mssql-py-core/src/connection.rs Creates shared clients and sync cursors.
mssql-py-core/src/lib.rs Registers the new Python class.
mssql-py-core/tests/test_sync_cursor.py Adds live SQL Server tests.
mssql-py-core/tests/rs-only-tests/test_sync_async_cursor_mock.py Adds mock-server path and transition tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.


/// Python synchronous Cursor class driving the reactor-free sync core.
#[pyclass]
pub struct PyCoreSyncCursor {
Comment on lines +192 to +199
pub(crate) fn is_on_rows(cell: &SharedClient) -> Result<bool, PyErr> {
let guard = cell.lock().map_err(|_| poisoned())?;
Ok(match &*guard {
PyClient::Async(c) => c.on_rows(),
PyClient::Sync(s) => !s.get_metadata().is_empty(),
PyClient::Transitioning | PyClient::Dead(_) => false,
})
}
Comment on lines +108 to +111
bad_query = (
"SELECT CAST(value AS INT) AS n "
"FROM (VALUES ('1'), ('2'), ('notanumber')) AS t(value)"
)
Comment on lines +264 to +267
try:
conn = _connect(ctx)
except Exception as exc: # noqa: BLE001 - env-sensitive TLS handshake
pytest.skip(f"TLS connect to mock unavailable in this environment: {exc}")
conn = mssql_py_core.PyCoreConnection(client_context)
try:
cursor = conn.sync_cursor()
cursor.execute("SELECT 1 AS value UNION ALL SELECT 2 UNION ALL SELECT 3")
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as draft August 10, 2026 06:49
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as ready for review August 10, 2026 07:40
@saurabh500
Saurabh Singh (saurabh500) marked this pull request as draft August 10, 2026 13:28
…ync cursors

Expose both a first-class sync cursor and the existing async cursor from one
shared sans-I/O protocol core. Replace the connection's Arc<Mutex<TdsClient>>
cell with Arc<std::sync::Mutex<PyClient>>, where PyClient flips in place between
the async TdsClient edge and the reactor-free TdsSyncClient edge (Transitioning
sentinel between take and store; Dead on unrecoverable flip/revert). Cursors
clone the Arc to the cell, so ownership-by-value flips never need
Arc::try_unwrap.

The new PyCoreSyncCursor flips to the sync edge only after execute resolves
metadata with rows pending, pulls rows via TdsSyncClient::next_row_into with no
block_on, and reverts to async before any control-plane op or on a fetch error
(recover via into_async drain). TLS connections report NotEligible, so the sync
cursor transparently falls back to the async block_on path byte-identically.
The async PyCoreCursor keeps its public Python API and never flips. rowcount is
an additive read-only property sourced from last_rows_affected() on the async
edge before any flip, so sync == async by construction.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e2c378f8-3ba1-4b48-9ebe-5a4ea2bd2761
@saurabh500
Saurabh Singh (saurabh500) force-pushed the dev/saurabh/rewire-py-core-sync-async-cursor-l6 branch from c08aea3 to 452caa9 Compare August 10, 2026 21:17
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.

2 participants