feat(sdk): Phase 11.7 SDK propagation of Busy / BusySnapshot (SQLR-22)#128
Merged
feat(sdk): Phase 11.7 SDK propagation of Busy / BusySnapshot (SQLR-22)#128
Conversation
Surfaces retryable engine errors through the C FFI and every
language SDK so Python / Node / Go callers can actually write
BEGIN CONCURRENT retry loops with idiomatic per-language
patterns. Picked ahead of plan-doc 11.5 (checkpoint integration)
for the same reason 11.5 / 11.6 jumped the queue — durability
already works through the legacy save_database mirror, but
SDK users hitting BEGIN CONCURRENT had no way to distinguish
retryable errors from real failures.
Plan-doc 11.8 ("SDK + REPL propagation") split into two:
- FFI/SDK error propagation ships here as roadmap 11.7
- Multi-handle SDK shape + REPL .spawn → roadmap 11.10
C FFI:
- new SqlriteStatus::Busy = 5 + BusySnapshot = 6 codes
- SqlriteStatus::is_retryable() covers both
- new status_of_sqlrite() mapper routes engine-typed errors to
the dedicated codes; generic status_of() keeps mapping every
error to Error for non-engine result types
- sqlrite_execute switched to the engine-aware mapper
- header regenerated via build.rs
Python SDK:
- new sqlrite.BusyError + sqlrite.BusySnapshotError pyo3
exception classes, both inheriting from sqlrite.SQLRiteError
- new map_engine_err() helper inspects the engine variant and
raises the matching exception class
- all engine-typed call sites (open / execute / prepare /
query / rows.next) routed through it
- existing `except sqlrite.SQLRiteError` blocks still catch
both; retry helpers branch with `except sqlrite.BusyError`
Node.js SDK:
- new exported ErrorKind string enum ('Busy' | 'BusySnapshot'
| 'Other') and errorKind(message) classifier function
- engine's thiserror Display already prefixes the error
message with 'Busy: ' / 'BusySnapshot: '; classifier matches
the prefix (longest-first to avoid mis-classifying snapshot
errors)
- JS pattern: try { ... } catch (err) {
if (errorKind(err.message) === ErrorKind.Busy) continue;
throw err;
}
Go SDK:
- new ErrBusy + ErrBusySnapshot sentinel errors
- new IsRetryable(err error) bool helper covers both
- wrapErr recognises new FFI status codes and wraps the
engine message with fmt.Errorf("…: %w", ErrBusy) so
errors.Is(err, sqlrite.ErrBusy) works through the cgo +
database/sql driver chain
WASM SDK: deliberately untouched. Browser is single-threaded;
multi-handle Connection::connect concurrency isn't exposed
through wasm-bindgen yet. Same 'Busy: ' message prefix will
be the classifier hook when the multi-handle WASM work lands
(11.10).
Subtle issues hit:
- napi-rs derive #[napi(string_enum)] auto-adds Clone+Copy.
My manual `#[derive(Debug, Clone, Copy, ...)]` produced
E0119 (conflicting impls). Dropped Clone+Copy from the
manual derive list.
- Python's `SQLRiteError` short name clashes between the
engine enum and the pyo3 exception class (both called
SQLRiteError). Used fully-qualified
`sqlrite::SQLRiteError::Busy(_)` in match arms.
Tests:
- 2 new FFI tests: is_retryable_covers_busy_variants +
begin_concurrent_busy_status_round_trip (exercises a real
conflict over a shared backing DB via Connection::connect
through FFI).
- 3 Node-side Rust tests for classify_error_message.
- 2 Python tests (class existence + inheritance, journal_mode
round-trip).
- 3 Go tests (sentinel distinctness, IsRetryable coverage,
journal_mode round-trip).
What this slice doesn't do:
- Each SDK's connect() still builds an independent backing DB.
End-to-end testing of cross-handle Busy needs the multi-handle
SDK shape (planned as 11.10). This PR ships the *plumbing* so
once that wiring lands, callers already have the retry idioms.
- WAL log-record durability still deferred to 11.8.
- SDK READMEs not yet updated with retry examples — that's a
doc sweep, lands alongside 11.11.
652/652 Rust workspace tests pass. 3/3 Node-side classifier
tests pass. fmt + clippy + doc clean on changed files. Python
+ Go SDK CI jobs will exercise the SDK-side additions on the
next run.
Roadmap renumbered: plan-doc 11.5 (checkpoint) → roadmap 11.8;
plan-doc 11.7 (indexes) → 11.9; multi-handle SDK + REPL =
11.10; plan-doc 11.9 (docs) → 11.11. Called out in the
roadmap entries so plan-doc references remain readable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Go: my Phase 11.7 tests referenced the new sentinels and helper as
bare names (`ErrBusy`, `IsRetryable`), but `sdk/go/sqlrite_test.go`
is `package sqlrite_test` (external) and needs the `sqlrite.`
prefix. CI's `go test ./...` caught it; my workspace cargo build
didn't (Go SDK isn't a Rust workspace member).
Python: my `test_journal_mode_pragma_round_trips_through_python`
asserted `cur.fetchone() == ('mvcc',)` after `PRAGMA journal_mode`,
but PRAGMA goes through the cursor's non-query path — the rendered
single-row result lives in `CommandOutput.rendered`, never surfaced
through the cursor's row iterator. Fetchone() returns None.
Reshaped both tests to verify the *gate* opens rather than the
read-form renders: `PRAGMA journal_mode = mvcc` followed by a
`BEGIN CONCURRENT` that would otherwise reject. Same property,
cleanly testable through each SDK's public API today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Surfaces retryable engine errors through the C FFI and every language SDK so Python / Node / Go callers can actually write
BEGIN CONCURRENTretry loops with idiomatic per-language patterns. Picked ahead of plan-doc 11.5 (checkpoint integration) for the same reason 11.5 / 11.6 jumped the queue — durability already works through the legacysave_databasemirror, but SDK users hittingBEGIN CONCURRENThad no way to distinguish retryable errors from real failures.Plan-doc 11.8 ("SDK + REPL propagation") split into two: FFI/SDK error propagation ships here as roadmap 11.7; multi-handle SDK shape + REPL
.spawn→ roadmap 11.10.What ships
C FFI (
sqlrite-ffi/src/lib.rs)SqlriteStatus::Busy = 5+SqlriteStatus::BusySnapshot = 6codesSqlriteStatus::is_retryable()covers bothstatus_of_sqlritemapper inspects the engine'sSQLRiteErrorvariant and routes to the dedicated codes; the genericstatus_ofkeeps mapping every error toErrorfor non-engine resultssqlrite_executeswitched to the engine-aware mapperbuild.rsPython SDK (
sdk/python/src/lib.rs)sqlrite.BusyErrorandsqlrite.BusySnapshotErrorpyo3 exception classes, both inheriting fromsqlrite.SQLRiteErrormap_engine_errhelper raises the matching exception based on the engine variantexcept sqlrite.SQLRiteErrorblocks still catch bothNode.js SDK (
sdk/nodejs/src/lib.rs)ErrorKindstring enum ('Busy'|'BusySnapshot'|'Other')errorKind(message: string)classifier — regex-matches the engine'sthiserrorprefix ('Busy: '/'BusySnapshot: ')BusySnapshotdoesn't mis-classify asBusyGo SDK (
sdk/go/sqlrite.go)sqlrite.ErrBusyandsqlrite.ErrBusySnapshotsentinel errorssqlrite.IsRetryable(err error) boolhelper covers bothwrapErrrecognises the new FFI status codes and wraps the engine message withfmt.Errorf("…: %w", ErrBusy)soerrors.Is(err, sqlrite.ErrBusy)works through the cgo +database/sqldriver chainWASM SDK
Deliberately untouched. Browser is single-threaded; multi-handle
Connection::connectconcurrency isn't exposed throughwasm-bindgenyet. Same'Busy: 'message prefix will be the classifier hook when the multi-handle WASM work lands (11.10).What this slice doesn't do
connect()still builds an independent backing DB. End-to-end testing of cross-handleBusyrequires the multi-handle SDK shape (planned as 11.10). This PR ships the plumbing — once that wiring lands, callers already have the retry idioms they need.Test plan
cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets— cleancargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks— 652/652 (2 new FFI tests including a real cross-handle conflict via the FFI'sConnection::connect)cargo test -p sqlrite-nodejs— 3 new classifier tests passcargo build -p sqlrite-ffi -p sqlrite-python -p sqlrite-nodejs— all cleancargo clippy— no new warnings on changed filescargo fmt --all -- --check— cleancargo doc— no new warnings on changed filessdk/python/tests/test_sqlrite.py+sdk/go/sqlrite_test.go)New tests
is_retryable_covers_busy_variants,begin_concurrent_busy_status_round_trip(exercises a real conflict over a shared backing DB viaConnection::connectthrough the FFI handle).classify_recognises_busy_prefix,classify_recognises_busy_snapshot_prefix,classify_returns_other_for_generic_errors.test_busy_error_class_exists_and_inherits_from_sqlrite_error,test_journal_mode_pragma_round_trips_through_python.TestBusySentinelsAreDistinctErrors,TestIsRetryableCoversBothSentinels,TestJournalModeMvccRoundTripsThroughGoDriver.Subtle issues hit + fixed
napi-rs's#[napi(string_enum)]auto-derivesClone + Copy. My manual#[derive(Debug, Clone, Copy, …)]clashed with E0119 (conflicting implementations). DroppedClone + Copyfrom the manual derive list.SQLRiteErrorshort name clashes between the engine enum and the pyo3 exception class (both calledSQLRiteError). Used fully-qualifiedsqlrite::SQLRiteError::Busy(_)in match arms to disambiguate.Roadmap renumbering
.spawn(was plan-doc 11.8's other half) → roadmap 11.10Called out in the roadmap entries so plan-doc references remain readable.
🤖 Generated with Claude Code