Rewire mssql-py-core onto the reactor-free sync core (opt-in sync cursor) - #205
Conversation
c08aea3 to
d9dba58
Compare
a0d95c0 to
c08aea3
Compare
a8b9f5a to
c08aea3
Compare
There was a problem hiding this comment.
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
PyCoreSyncCursorand 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 { |
| 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, | ||
| }) | ||
| } |
| bad_query = ( | ||
| "SELECT CAST(value AS INT) AS n " | ||
| "FROM (VALUES ('1'), ('2'), ('notanumber')) AS t(value)" | ||
| ) |
| 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") |
…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
c08aea3 to
452caa9
Compare
|
Closing, consistent with the rest of the sans-I/O stack (#189–#203, closed 2026-08-13). This PR is an L5–L7 consumer of the reactor-free sync core, so it has no base once that core is not landing. The stack was closed because the row-decode performance work that motivated it was re-scoped around #247, where benchmarked spikes showed the dominant win came from a far smaller change than inverting the core to a sync That argument has since been settled by measurement rather than projection. PR #264 has now merged ( Two further results from that work are worth recording here, because both cut against restructuring this path speculatively:
This is cleanup, not a rejection of the analysis. The branch is deliberately not deleted, so the work remains recoverable if the direction is revisited. |
Description
L6 — Rewire
mssql-py-coreonto the reactor-free sync TDS core.Stacked on the frozen L5 tip (
dev/saurabh/rewire-odbc-sync-core-l5@5c09f0e4). Append-only single commit; base = L5 branch, notmain. Scope ismssql-py-coreonly — 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) }behindArc<std::sync::Mutex<PyClient>>(pyclient.rs). TheArcpoints at the cell, soArc::try_unwrapis never used — cursors keep their clone and match the enum under the lock.std::mem::replace(&mut *guard, Transitioning)→ callinto_sync/into_asyncon the moved-out value → store the result variant back.Transitioningis a sentinel that only exists between take and store (no early-return while transitioning); an unrecoverable flip storesDead(String)and surfacesErr.into_sync()runs insidehandle.enter()so the runtime handle is captured for the later revert (a nakedinto_sync()off-runtime would captureNoneand poisoninto_async).Asyncat end-of-rows / beforeclose_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 theSync(TdsSyncClient)arm and drivenext_row_intowith noblock_on; TLS/non-eligible transports fall back to theblock_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: itsblock_onblocks 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
rowcountcaptured fromlast_rows_affected()pre-flip on the async arm (faithful-count parity contract, populated by the sharedapply_row_read_tokencount_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_pydrivingnext_row_into().awaiton theAsyncarm, 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. Nopyo3-async-runtimesdependency is added here;Cargo.lockis unchanged.Build / test path
mssql-py-coreis edition 2024 and excluded from the cargo workspace, so the plaincargo b*aliases do NOT cover it — validated viascripts\bfmt.ps1+scripts\bclippy.ps1(both cover py-core) +maturin+ the Python suite through the existingconftest.pyfixtures.Tests
block_on-over-async path) — env-skips locally on the pre-existing mock-TLS-on-Windows timeout; CI-validated.block_on-backed cursor expose the same surface.fetchoneresults identical to theblock_onpath.into_asyncdrain, connection reused (validated live: SQL 245 conversion error mid-stream).Gates:
scripts\bfmt.ps1clean,scripts\bclippy.ps1(-D warnings) clean (workspace + py-core),maturin developbuildsmssql_py_core+mssql_mock_tds, mock cursor suite = 10 passed / 1 env-skip.Checklist
cargo bfmtpassescargo bclippypassescargo btestpassesStack 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)