Metadata-Version: 2.5
Name: sqlalchemy-libsql-native
Version: 0.1.0
Summary: SQLAlchemy compatibility DBAPI facade + dialect for remote libSQL (Turso).
Project-URL: Repository, https://github.com/keelsonhq/sqlalchemy-libsql-native
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: <3.13,>=3.12
Requires-Dist: libsql==0.1.11
Requires-Dist: sqlalchemy<2.1,>=2.0.51
Description-Content-Type: text/markdown

# sqlalchemy-libsql-native

**SQLAlchemy compatibility DBAPI facade + dialect** for remote
libSQL (Turso managed libSQL / Hrana).

> **Note**: This repository is a read-only release mirror. Development happens in the private Keelson monorepo; issues are welcome here, but pull requests are not accepted — changes land through the next release.

Wraps the frozen upstream driver
(`libsql==0.1.11`, exact pin) without forking it, and adds the minimal contract
SQLAlchemy needs for exception classification and disconnect recovery.

This is a **bridge, not a permanent artifact**: it exists to make the current
frozen driver usable with SQLAlchemy today, and is expected to shrink or retire
as upstream grows a real DBAPI surface. Full PEP 249
conformance is **not** claimed; the goal is limited to making SQLAlchemy's
exception-classification and disconnect-recovery machinery work correctly.

Design goals:

- Raw driver errors (`builtins.ValueError`) are classified into a PEP 249-shaped
  exception hierarchy, structured `SQLITE_CONSTRAINT*` codes taking priority
  over message text, with an unknown error failing closed to `DatabaseError`.
- Hrana stream loss/expiry maps to `OperationalError` so SQLAlchemy's
  `is_disconnect` / `pool_pre_ping` invalidation and recovery work; nothing else
  is ever treated as a disconnect.
- The dialect owns the URL/argument contract and the pool policy (NullPool for
  remote URLs by default); the adapter never retries on the caller's behalf.
- Deterministic, self-contained fault injection (no external services beyond
  Docker) proves the above, including commit ambiguity on a lost commit
  acknowledgement.

## Invariants

- **The package is standalone**: no imports from any host application; shipped
  helpers (`sqlalchemy_libsql_native.testing`) let downstream test harnesses
  import them without a dependency on this repo's test-only files.
- **No cloud credentials** in this package.
- Driver is **exact-pinned** (`libsql==0.1.11`); supported SQLAlchemy versions
  are `>=2.0.51,<2.1`.
- `src/` layout, hatchling build backend, Python `>=3.12,<3.13`.

## Layout

```
pyproject.toml
src/sqlalchemy_libsql_native/
  __init__.py
  errors.py     # PEP 249-shaped exception hierarchy
  classify.py   # raw builtins.ValueError -> facade exception (structured code first)
  dbapi.py      # ConnectionProxy / CursorProxy facade (connect args, raising attribute boundaries)
  dialect.py    # import_dbapi / create_connect_args / on_connect / is_disconnect / pool policy
  testing.py    # generic Tier 2/3 helpers: local sqld + fault injection + asserts
tests/
  test_errors.py test_classify.py test_dbapi.py test_dialect.py   # Tier 0 unit
  test_file_integration.py                                        # Tier 1 (real driver, file SQLite)
  test_local_sqld_integration.py                                  # Tier 2 (real driver, local Hrana)
```

## Running the tests

Test tiers: **Tier 0** — pure-Python unit tests (no driver). **Tier 1** — real
driver against an embedded file SQLite DB (no network). **Tier 2** — real driver
against a local `libsql-server` (Hrana) in Docker. **Tier 3** — downstream
integration against a real managed deployment; those suites live outside this
package but reuse the shipped `sqlalchemy_libsql_native.testing` helpers.

### Tier 0 + Tier 1 (self-contained, no server — always green)

```bash
uv sync --group dev
uv run pytest tests/ -q
```

Tier 0 (`classify`/`errors`) is pure Python. Tier 1 exercises the real
`libsql==0.1.11` driver against an embedded **file** SQLite DB (no network). Tier 2
below **skips with a reason** when Docker is unavailable, so this command stays
green everywhere.

### Tier 2 (local libsql-server / Hrana — needs Docker)

Reproduces the **remote** transport that file mode cannot: remote constraint
classification `(a)`, transparent disconnect recovery `(b)`, and the three
fault-injection points. Self-contained (a local server in Docker — no Turso Cloud,
no credentials).

```bash
# Requires: `docker` on PATH + a running Docker daemon.
uv run pytest tests/test_local_sqld_integration.py -v
```

The server image is **pinned by tag + digest** (a moved tag cannot silently change
the image):

```
ghcr.io/tursodatabase/libsql-server:v0.24.32
  @sha256:dedf5273da945d90f3b85fe367ed8f821349117ac2c488bf43c4a6bf9f2123e0
```

Auxiliary observations:

- Natural idle stream-expiry (~330–360 s: 10 s expiration + 300 s cleanup) is the
  slow, non-deterministic counterpart to the deterministic `(b)` injection. It is
  **opt-in** (default-skipped): run with `LIBSQL_NATIVE_SLOW_EXPIRY=1`.
- `(c)` concurrent writes is **observation only** — success is not asserted; the
  invariant is only that any surfaced error is facade-classified (never a raw
  `ValueError`). On v0.24.32 the collapse surfaces as `TRANSACTION_TIMEOUT`
  (→ `DatabaseError`) / occasionally `STREAM_EXPIRED` (→ `OperationalError`).

### Commit-response disconnect injection

The "cut at commit response" fault (`point="commit_response"` in
`sqlalchemy_libsql_native.testing`) runs the **real commit first** — so the write is
durably applied server-side — and only then injects the disconnect, modelling a
lost acknowledgement. The Tier 2 test proves the resulting ambiguity concretely:
the caller receives a classified `OperationalError`, yet the row is present when
read back through a fresh connection, so "failure" does **not** mean "rolled back"
and a naive retry would double-apply. This is deterministic and self-contained (no
extra service). A network-level proxy (Toxiproxy) is documented in `testing.py` as
the higher-fidelity Tier 3 alternative; `point="commit"` is the complementary
request-side loss (fails before the commit reaches the server → not applied). In
all cases the connection is invalidated and the adapter does not retry — retry
policy is deliberately left to the caller.

## Generic test helpers (`sqlalchemy_libsql_native.testing`)

The sqld launcher, fault injector, and classification asserts live in the
**shipped package** (not `tests/`) so downstream test harnesses can import them
from the installed distribution without a dependency on this repo's test-only
files. Key surface:
`LibsqlServer`, `fault_injecting_engine`, `FaultController`, `docker_available`,
`assert_integrity_surfaced`, `assert_disconnect_surfaced`.
