# Reliability & Accuracy

> **Status:** v0.1 — first pass, finalized 2026-05-15
> (review-pass verified all file:line citations against the
> live source). Last updated 2026-05-15.
> A federation-sibling-scoped reliability audit: walked the two
> source files (`_engine.py`, `_session.py`) end-to-end against
> the **RA** / **EH** / **SC** rule set from
> `mgf-standard/docs/standards/`. The sibling's public surface is
> three names (`create_engine`, `create_sessionmaker`,
> `tenant_session`) — narrow, but the RLS-tenant-scoping helper
> is the load-bearing security boundary for every multi-tenant
> consumer.
>
> **Scope:** v1.0 readiness for mgf-sqlalchemy.
> **Audience:** the maintainer + future AI agents doing
> reliability work on this sibling.
> **Cross-references:** [`PUBLIC_API.md`](../../PUBLIC_API.md),
> [`docs/standards/README.md`](../standards/README.md),
> [`mgf-common/docs/inprogress/RELIABILITY_AND_ACCURACY.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/inprogress/RELIABILITY_AND_ACCURACY.md)
> (cornerstone), [`mgf-common/docs/design/multi_tenancy.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/design/multi_tenancy.md)
> (the design paper that motivates `tenant_session`).

The sibling is at v0.2.0 (post-v2.6/v2.7 compliance bump from
v0.1.1). The surface is small but the `tenant_session` helper
is the SQL-level isolation primitive every multi-tenant
consumer depends on; reliability gaps here are
security-relevant.

---

## §1 You are here

mgf-sqlalchemy ships three public names; `mypy --strict`
clean; ruff clean; the `tenant_session` helper validates UUIDs
BEFORE interpolation (the SQL-injection defence); SQLite vs
non-SQLite pool semantics are correctly detected at engine
construction.

What this tracker exposes is the **contract-clarity shape** —
invariants the implementation holds correctly but the docs
don't quite spell out, plus ergonomic gaps where the
`**extra` shape (or its absence) leads to surprising
behaviour.

**Counts (2026-05-17):**

| Priority | Open | Closed | Total |
|---|---|---|---|
| P0 — v1.0 surface-lock blockers | 0 | 0 | 0 |
| P1 — should land before/with v1.0 | 0 | 2 | 2 |
| P2 — post-v1.0 hardening | 0 | 3 | 3 |
| **Total** | **0** | **5** | **5** |

**Do first:** all items closed — tracker complete for v1.0
readiness (RA-01..RA-05, closed across 2026-05-16 / 2026-05-17).

---

## §2 Priority table

| ID | Concern | One-line | Effort | Status |
|---|---|---|---|---|
| [**RA-01**](#ra-01-tenant_session-transaction-state-contract) | Contract clarity / security boundary | `tenant_session` runs `SET LOCAL app.current_tenant = '<uuid>'` — `LOCAL` scopes to the active transaction. The function relies on SQLAlchemy 2's auto-begin behaviour to start one but doesn't document this; consumers expecting `tenant_session` to "just work" outside a transaction may see surprising behaviour. | S | ✅ closed |
| [**RA-02**](#ra-02-create_engine-extra-override-semantics) | Surprising kwarg precedence | `create_engine` does `kwargs.update(extra)` after assembling defaults — `**extra` SILENTLY overrides the explicit `pool_size=` / `pool_recycle=` / etc. kwargs. Documented as "rare options" but the override behaviour isn't called out. | XS | ✅ closed |
| [**RA-03**](#ra-03-create_sessionmaker-no-extra-kwargs) | Ergonomic gap | `create_sessionmaker(engine, *, expire_on_commit=False)` takes only one kwarg; consumers needing other `async_sessionmaker` options (`autoflush=False`, `info=…`, `class_=CustomAsyncSession`) must construct their own. Documented choice but a `**extra` opening would help. | XS | ✅ closed |
| [**RA-04**](#ra-04-nil-uuid-accepted-in-tenant_session) | Defensive policy | `tenant_session(session, "00000000-0000-0000-0000-000000000000")` is accepted (NIL UUID is a valid UUID). May be intentional ("system rows", "unassigned tenant") OR may be a misconfig. Document the policy explicitly. | XS | ✅ closed |
| [**RA-05**](#ra-05-sqlite-detection-shape) | Brittle URL prefix check | `is_sqlite = database_url.startswith(("sqlite://", "sqlite+"))` matches every SQLite driver and only them; correct today, but `sqlcipher+pysqlcipher://` style URLs (encrypted-sqlite drivers) wouldn't match. Defensive future-proofing. | XS | ✅ closed |

Effort scale: XS (≤ 15 min); S (≤ 1 h); M (≤ 1 session); L
(multiple sessions).

---

## §3 Invariants we hold

The list of "closed elsewhere; do not re-open":

- **RA-INV-1** — UUID validation runs **BEFORE** any SQL is
  emitted. `tenant_session`
  ([`_session.py:87-94`](../../src/mgf/sqlalchemy/_session.py#L87))
  calls `UUID(tenant_id)` (which raises `ValueError` on
  malformed input) BEFORE constructing the SQL string. SQL
  injection through `tenant_id` is structurally impossible.
- **RA-INV-2** — `SET LOCAL` qualifier scopes to the active
  transaction. Documented at
  [`_session.py:63-64`](../../src/mgf/sqlalchemy/_session.py#L63).
  Auto-resets on commit / rollback; cross-tenant leakage
  between sessions / requests is impossible at the SQL
  layer, GIVEN a transaction is active.
- **RA-INV-3** — SQLite gets a different pool config.
  `create_engine` detects `sqlite://` / `sqlite+` URLs and
  skips `pool_size` / `max_overflow` kwargs
  ([`_engine.py:75-78`](../../src/mgf/sqlalchemy/_engine.py#L75)).
  StaticPool / NullPool don't accept those.
- **RA-INV-4** — `expire_on_commit` defaults to `False`
  ([`_session.py:39`](../../src/mgf/sqlalchemy/_session.py#L39)),
  matching SQLAlchemy 2.x's recommendation for async usage.
  Committed objects stay accessible across awaits.
- **RA-INV-5** — `create_engine` returns an **unconnected**
  engine. First-connection is lazy on the first `await
  engine.connect()` or sessionmaker call. Documented at
  [`_engine.py:62-64`](../../src/mgf/sqlalchemy/_engine.py#L62).

---

## §4 Work units

### RA-01 `tenant_session` transaction-state contract

**Why:** `tenant_session`
([`_session.py:56-99`](../../src/mgf/sqlalchemy/_session.py#L56)):

```python
@asynccontextmanager
async def tenant_session(session, tenant_id):
    # (UUID validation)
    await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_str}'"))
    yield session
```

The docstring says "The `LOCAL` qualifier scopes to the
current transaction" — correct — but the example doesn't
show how to ENSURE a transaction is active:

```python
async with sessionmaker() as session:
    async with tenant_session(session, tenant_id) as scoped:
        rows = await scoped.execute(text("SELECT ..."))
```

This works **because** SQLAlchemy 2.x's `AsyncSession`
auto-begins a transaction on the first `.execute()` (the
`SET LOCAL` one). Subsequent queries within the
`async with tenant_session(...)` block use the same auto-
begun transaction. When the `async with` exits, the surrounding
`async with sessionmaker() as session` triggers `session.close()`
→ implicit `session.rollback()` → the GUC resets per
`SET LOCAL` semantics.

This works **today** but the contract is fragile:

- A future SQLAlchemy major version that disables autobegin
  by default would break this silently — the SET LOCAL would
  not be attached to any transaction.
- A consumer who calls `session.commit()` AFTER `tenant_session`
  yields but BEFORE re-entering it expects the next query to
  still be tenant-scoped — but the commit resets the GUC.
  The consumer must understand this.
- Calling `tenant_session` on a session that's in the middle
  of a transaction works (the SET LOCAL attaches to that
  transaction), but the consumer might expect "scoping" to
  mean "restored on exit" — which is NOT what `LOCAL` does
  (it auto-resets on commit/rollback, not on `async with`
  exit).

**Concrete output:**

- Expand the docstring with a "Transaction-state contract"
  section:
  - "This helper requires the session to be in a transaction
    when the yield body runs. SQLAlchemy 2.x AsyncSession's
    autobegin behaviour makes the `SET LOCAL` itself START
    a transaction; subsequent queries use it. On
    `session.commit()` or `.rollback()` inside the yield body,
    the GUC resets and subsequent queries are NOT
    tenant-scoped."
  - Show two example shapes:
    1. Read-only: `async with sessionmaker() as s: async with tenant_session(s, tid) as scoped: rows = await scoped.execute(...)`
    2. Read-write with explicit transaction: `async with sessionmaker() as s: async with s.begin(): async with tenant_session(s, tid) as scoped: ... await s.commit()` — NOTE the commit moves the GUC out of scope.
- Add a runtime sanity check (optional, P2): after the SET
  LOCAL runs, call `session.in_transaction()`; if False
  (autobegin disabled in some future config), raise
  `RuntimeError("tenant_session: session is not in a transaction; SET LOCAL has no effect")`.
- Add an integration test (against real Postgres if
  available, or a SQLite-style mock) that exercises both
  shapes.

**Locked in by:** the integration test pinning both shapes;
a future SQLAlchemy autobegin change breaks the read-only
example.

**Cross-references:** the `mgf-common/docs/design/multi_tenancy.md`
paper has the broader RLS-policy contract; this helper is
the GUC-setting half. The other half (the `CREATE POLICY` on
the consumer's tables) is consumer-side runbook content.

---

### RA-02 `create_engine` `**extra` override semantics

**Why:** `create_engine`
([`_engine.py:66-81`](../../src/mgf/sqlalchemy/_engine.py#L66)):

```python
kwargs: dict[str, Any] = {
    "echo": echo,
    "pool_pre_ping": pool_pre_ping,
    "pool_recycle": pool_recycle,
}
is_sqlite = ...
if not is_sqlite:
    kwargs["pool_size"] = pool_size
    kwargs["max_overflow"] = max_overflow
kwargs.update(extra)
return create_async_engine(database_url, **kwargs)
```

The `kwargs.update(extra)` at the bottom means **`**extra`
silently overrides every explicit kwarg above**. A consumer
who does:

```python
create_engine("postgresql+asyncpg://...", pool_size=20, **legacy_kwargs)
```

where `legacy_kwargs = {"pool_size": 5}` ends up with
`pool_size=5` (the extra wins). The explicit kwarg looks
authoritative but isn't.

The docstring says "`**extra` is forwarded ... for rare
options (`connect_args`, ...)" — but doesn't call out the
override behaviour.

**Concrete output:**

- Add an explicit collision check: before `kwargs.update(extra)`,
  iterate the `extra` keys and raise `ValueError` if any
  collide with the explicit kwargs (`echo`, `pool_pre_ping`,
  `pool_recycle`, `pool_size`, `max_overflow`).
- Update the docstring to call out the collision behaviour:
  "`**extra` MAY contain only options not already named in
  the signature — collisions raise `ValueError`. For
  documented kwargs, use the explicit parameter."
- Add a unit test asserting the ValueError fires on
  collision.

**Locked in by:** the unit test pins the boundary; a
regression that drops the collision check fails it.

**Cross-references:** AP-04 (parameter additions are
non-breaking iff there's no collision).

---

### RA-03 `create_sessionmaker` no `**extra` kwargs

**Why:** `create_sessionmaker`
([`_session.py:36-53`](../../src/mgf/sqlalchemy/_session.py#L36))
takes only `engine` + `expire_on_commit`. A consumer needing
other `async_sessionmaker` options (`autoflush=False`,
`info={...}`, `class_=CustomAsyncSession`) has to construct
their own:

```python
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession

sessionmaker = async_sessionmaker(
    engine,
    class_=MyAsyncSession,
    expire_on_commit=False,
    autoflush=False,
)
```

That's not terrible — it's a one-liner — but it duplicates
the `expire_on_commit=False` default that `mgf.sqlalchemy`
specifically codifies. Future SQLAlchemy versions might
change the default; consumers using their own factory
miss the update.

**Concrete output:**

- Add `**extra: Any` to `create_sessionmaker`'s signature.
- Forward to `async_sessionmaker(engine, class_=AsyncSession,
  expire_on_commit=expire_on_commit, **extra)`.
- Apply the same collision-check pattern as RA-02 — `extra`
  may not include `class_` or `expire_on_commit`.
- Add a unit test exercising a non-default kwarg (e.g.,
  `autoflush=False`) and asserting the resulting sessionmaker
  honours it.

**Locked in by:** the unit test pins the forwarded kwarg.

**Cross-references:** AP-05 (kwarg-only signature shapes); RA-02
(same collision-check pattern).

---

### RA-04 NIL UUID accepted in `tenant_session`

**Why:** `tenant_session(session, "00000000-0000-0000-0000-000000000000")`
is accepted today — `UUID("00000000-...")` returns the NIL
UUID without error. Same for the version-1 UUID format
(time-based, includes MAC), version-3/5 (name-based), etc.
All accepted.

The NIL UUID is sometimes used as a sentinel for "system
rows" or "unassigned tenant." But it's ALSO a common
default in misconfigured systems — a `tenant_id` field that
defaults to `UUID()` in some ORMs is the NIL UUID.

A misconfigured consumer could end up with every "tenant-scoped"
query running with `app.current_tenant = '00000000-...'`,
which their RLS policy may or may not handle correctly
(the most common policy `tenant_id = current_setting(...)`
returns rows tagged with the NIL tenant, which is probably
test data — leakage between "real" and "system" tenants).

**Concrete output:**

- Pick one:
  1. **Strict default + opt-out flag.** Add `allow_nil_uuid:
     bool = False` parameter; reject NIL UUID unless the
     consumer explicitly opts in.
  2. **Document explicitly.** Add to the docstring: "NIL UUID
     (`00000000-0000-0000-0000-000000000000`) is accepted; the
     library does not reserve it for any special meaning. If
     your application uses the NIL UUID as a 'no tenant'
     sentinel, ensure your RLS policy handles it
     appropriately."
- The strict-default shape is safer but is a breaking change.
  Documentation-only is the safer post-v0.2 fix.
- Either way: a parametrised test exercising NIL UUID
  pinning the chosen behaviour.

**Locked in by:** the test; a behaviour change without
intent fails it.

**Cross-references:** SC-04 (atomic-write — unrelated); the
`mgf-common/docs/design/multi_tenancy.md` paper for the
RLS-policy contract that should handle this case
explicitly.

---

### RA-05 SQLite detection shape

**Why:** `is_sqlite = database_url.startswith(("sqlite://", "sqlite+"))`
([`_engine.py:75`](../../src/mgf/sqlalchemy/_engine.py#L75)).

The check correctly matches every standard SQLite URL:

- `sqlite:///path/to/db.sqlite`
- `sqlite+aiosqlite:///path/to/db.sqlite`
- `sqlite+pysqlite:///path/to/db.sqlite`
- `sqlite:///:memory:`

But:

- `sqlcipher+pysqlcipher://path/to/encrypted.db` — uses
  the SQLCipher driver (encrypted SQLite). Same pool
  semantics as SQLite (StaticPool / NullPool). Would NOT
  match the current check → `pool_size` + `max_overflow`
  get passed → SQLCipher would raise.
- Future SQLite-shaped drivers under different scheme
  names would have the same problem.

This is a future-proofing concern, not an active bug — no
current Magogi consumer uses SQLCipher. But the URL-shape
detection is brittle.

**Concrete output:**

- Reshape the detection to driver-based rather than
  URL-prefix-based:
  - Use `sqlalchemy.engine.URL.make_url(database_url)` to
    parse the URL.
  - Check `url.get_driver_name().startswith("aiosqlite")` (or
    similar driver-introspection check).
- OR: keep the prefix check but expand it to cover known
  SQLite-shaped drivers — `("sqlite://", "sqlite+",
  "sqlcipher://", "sqlcipher+")`.
- The first shape is more robust; the second is simpler.
  Pick based on whether SQLCipher / similar drivers
  actually land in our consumer set.

**Locked in by:** a parametrised test exercising each URL
shape; a regression on the detection logic fails the right
row.

**Cross-references:** AP-05 (kwarg signature stability — the
detection logic is internal so changes don't bump the
public API).

---

## §5 Closed items

- **RA-01** — `tenant_session` transaction-state contract.
  Added a runtime check after the `SET LOCAL` execute(): if
  `session.in_transaction()` returns False, raise
  `RuntimeError` whose message names the failure mode (the
  GUC has no effect; tenant scoping is bypassed) and points
  at the workaround (`async with session.begin(): ...`).
  Expanded the docstring with a new "Transaction-state
  contract (RA-01)" section showing two example shapes —
  read-only (the common case; autobegin starts the
  transaction; close() resets) and read-write-with-explicit-
  commit (warning: commit moves the GUC out of scope). A
  future SQLAlchemy version that disables autobegin by
  default would have silently bypassed tenant scoping
  pre-fix; now it fails loud at the SET LOCAL site.
  3 new unit tests pin both branches + the actionable
  error message. (2026-05-17, commit pending.)
- **RA-02** — `create_engine` `**extra` collision check.
  Investigation surfaced that Python's own kwarg routing
  prevents the user-facing collision scenario the tracker
  described — `create_engine(url, pool_size=20, **{"pool_size":
  5})` raises `TypeError` from CPython before our function
  runs. The collision check inside the function body remains
  as defensive code against a future refactor that removes a
  named parameter; without the check, a removed parameter's
  key would suddenly route into `**extra` and silently use
  the default. Expanded the `**extra` docstring to document
  the contract explicitly. 3 new tests pin the live shape
  (TypeError on user-side collision; non-colliding `**extra`
  flows through; the defensive check IS in source — catches
  a refactor that removes it). (2026-05-17, commit pending.)
- **RA-03** — `create_sessionmaker` opened up with `**extra`.
  Added `**extra: Any` to the signature; forwards to
  `async_sessionmaker(engine, class_=AsyncSession,
  expire_on_commit=expire_on_commit, **extra)`. Consumers can
  now pass `autoflush=False` / `info={...}` / etc. without
  rebuilding the factory from scratch (which would skip the
  `expire_on_commit=False` default). Defensive collision check
  mirrors RA-02 — reserves both named params (`expire_on_commit`,
  `class_`) via the module-level constant
  `_RESERVED_SESSIONMAKER_KEYS`; a programmatic collision raises
  `ValueError`. `class_` is reserved because mgf-sqlalchemy pins
  `AsyncSession` for the federation invariants — consumers
  needing a custom session class build their own factory
  directly. 6 new unit tests cover: forwarding (`autoflush`,
  `info`), Python-call-layer collision behaviour, reserved-key
  collision, refactor-safety guard on the reserved keyset, the
  defensive check is in source, and the no-extra baseline still
  works. (2026-05-17, commit pending.)
- **RA-04** — NIL UUID policy documented in `tenant_session`'s
  docstring under a new "NIL UUID policy (RA-04 / SH-03)" section.
  Chose Option 2 (documentation-only) per the tracker's
  recommendation — strict-default rejection would be a breaking
  change with no clear v1.0 trigger. The docstring spells out
  that NIL is accepted, the misconfig failure mode (every
  request reading NIL-tagged rows), and two safe shapes
  (reject at the application layer; add an RLS predicate that
  excludes NIL from user-scoped reads). Cross-references SH-03
  twin item and `mgf-common/docs/design/multi_tenancy.md`. 1
  new parametrised test (4 cases: canonical str, no-dash hex,
  brace form, UUID object) pins the acceptance contract —
  a future strictness change fails the row and forces a
  documented decision. (2026-05-17, commit pending.)
- **RA-05** — SQLite-shape detection expanded to cover SQLCipher.
  Extracted the URL-prefix tuple into a module-level constant
  `_SQLITE_SHAPED_URL_PREFIXES = ("sqlite://", "sqlite+",
  "sqlcipher://", "sqlcipher+")`. SQLCipher (encrypted SQLite)
  has the same pool semantics as SQLite — StaticPool / NullPool
  reject `pool_size` / `max_overflow` — so the existing
  pool-kwarg-strip logic now applies to it too. Chose the
  prefix-expansion shape over driver-name introspection
  (`make_url(...).get_driver_name()`) because the prefix
  tuple is constant-time + zero-allocation and the list is
  small enough to maintain by hand; the introspection
  alternative is documented in §4 RA-05 for a v1.1+ revisit
  if the prefix list grows. 5 new unit tests cover: the
  current aiosqlite path still works with stripped pool kwargs;
  the prefix constant contains all four documented prefixes;
  parametrised matrix of 7 SQLite-shaped URLs all match; and
  parametrised matrix of 6 server-DB URLs (postgresql, mysql,
  mariadb variants) all DON'T match. (2026-05-17, commit
  pending.)

---

## §6 Notes on methodology

This first pass walked the two source files (~150 LOC total)
end-to-end against:

- **RA** rule patterns from mgf-common's
  `inprogress/RELIABILITY_AND_ACCURACY.md`.
- **EH** rules from `mgf-standard/docs/standards/ERROR_HANDLING.md`
  — translation seams, idempotency.
- **SC** rules from `mgf-standard/docs/standards/SECURITY_STANDARD.md`
  — SQL-injection defence (UUID validation), secret handling.
- **CF** rules from `mgf-standard/docs/standards/CONFIGURATION.md`
  — pool defaults, environment-tier layering.

The audit method:

1. Read each source file end-to-end with the rule set in
   mind.
2. For every concern surfaced, verify `file:line` against the
   live source.
3. Cross-reference against the cornerstone's RA tracker and
   the design paper at `mgf-common/docs/design/multi_tenancy.md`.
4. Triage: items that don't tie to a rule + concrete evidence
   are observations, not tracker items.

Five items is right-sized for this tightly-scoped sibling.
Future passes should look for:

- **SQLAlchemy 2.x behaviour changes.** This sibling pins
  closely to SQLA 2.x semantics (autobegin, expire_on_commit
  default). A SQLA 3.0 would warrant a full re-audit.
- **Postgres-specific edge cases.** `SET LOCAL` is Postgres-
  specific; other DBs would either reject the SQL or silently
  set a useless GUC. The library assumes Postgres for the
  RLS pattern but doesn't enforce.
- **Connection-pool sizing review.** The `pool_size=5,
  max_overflow=10` defaults are SQLA 2.x's documented choices
  but may not match a consumer's actual concurrent-request
  profile.

When an item closes, move it to §5 with the closing-commit
SHA. When the next pass sharpens an item, rewrite it in place
and note the change in §1.
