|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | += database-mcp / ffi — Zig FFI layer |
| 4 | +:orientation: deep |
| 5 | + |
| 6 | +== Purpose |
| 7 | + |
| 8 | +The Zig implementation of the connection state machine from `../abi/`, plus |
| 9 | +live SQL execution against SQLite (linked via `@cImport("sqlite3.h")`) and |
| 10 | +live VQL / KQL / GQL execution against VeriSimDB / QuandleDB / LithoGlyph via |
| 11 | +child `curl` processes. Exposes the four standard cartridge symbols |
| 12 | +(`boj_cartridge_init`, `boj_cartridge_deinit`, `boj_cartridge_name`, |
| 13 | +`boj_cartridge_version`) and ~20 database-specific `db_*` exports. |
| 14 | + |
| 15 | +== Files |
| 16 | + |
| 17 | +[cols="1,4"] |
| 18 | +|=== |
| 19 | +| File | Role |
| 20 | + |
| 21 | +| `build.zig` | Zig build graph — produces the loadable shared library plus the unit-test binary. |
| 22 | +| `database_ffi.zig` | All of it. ~1200 lines. State machine, 16-slot connection pool, SQLite integration, VQL/KQL/GQL curl execution, 24 inline tests. |
| 23 | +| `zig-out/` | Build artefacts (gitignored). |
| 24 | +|=== |
| 25 | + |
| 26 | +== Invariants |
| 27 | + |
| 28 | +* **Mutex discipline.** All reads and writes to the global `connections` |
| 29 | + array are protected by `var mutex: std.Thread.Mutex`. Every C-ABI export |
| 30 | + opens with `mutex.lock(); defer mutex.unlock();`. Pure helpers |
| 31 | + (`isValidTransition`, `appendJsonEscaped`) do not need the lock. |
| 32 | +* **Two-phase pattern** for blocking I/O. `db_execute_vql` / `_kql` / `_gql` |
| 33 | + release the mutex during the `curl` call and re-acquire it afterwards, |
| 34 | + then **re-validate slot state** (`active && state == .querying`) before |
| 35 | + writing the result back. See lines 515–587 for the canonical pattern. |
| 36 | +* **Bounds checks before dereference.** All `*_ptr` / `*_len` pairs reject |
| 37 | + zero-length and oversize inputs (error codes `-6`, `-8`) before any |
| 38 | + `@memcpy`. Pointer safety is documented in-line with `SAFETY:` comments. |
| 39 | +* **Enum hardening.** All public exports use `std.meta.intToEnum` rather |
| 40 | + than `@enumFromInt` on `c_int` arguments, so an invalid enum returns `-1` |
| 41 | + instead of panicking. |
| 42 | +* **State-machine mirror.** `isValidTransition` (lines 81–88) is the exact |
| 43 | + graph from `SafeDatabase.idr`. Changes there must land in both files. |
| 44 | +* **Slot capacity** is `MAX_CONNECTIONS = 16`. Exhaustion returns `-1`. |
| 45 | +* **Error codes**: `-1` invalid slot / exhausted, `-2` invalid transition, |
| 46 | + `-3` no sqlite handle (wrong backend), `-4` sqlite3_exec failed, |
| 47 | + `-5` output buffer too small, `-6` URL/path empty or too long, `-7` curl |
| 48 | + failed, `-8` zero-length input/output. |
| 49 | + |
| 50 | +== Test/proof surface |
| 51 | + |
| 52 | +24 inline `test "..."` blocks in `database_ffi.zig` (lines 878–1189). Coverage: |
| 53 | + |
| 54 | +* State machine (lines 878–931): connect, disconnect, double-close, query |
| 55 | + lifecycle, error recovery, transition validation. |
| 56 | +* SQLite live execution (lines 933–1045): in-memory DB open, execute |
| 57 | + `CREATE`/`INSERT`/`SELECT`, multiple rows, invalid SQL → error state, |
| 58 | + non-sqlite slot rejection. |
| 59 | +* VeriSimDB / QuandleDB / LithoGlyph connection lifecycle (lines 1047–1169): |
| 60 | + URL storage, empty/oversize rejection, query lifecycle via manual state |
| 61 | + transitions (no live server in test). |
| 62 | +* Wrong-backend rejection (lines 1171–1188). |
| 63 | + |
| 64 | +Run with `cd ffi && zig build test`. |
| 65 | + |
| 66 | +== Read-first |
| 67 | + |
| 68 | +. Lines 14–57 — types and the `ConnectionSlot` struct; note `url_buf[512]` for the HTTP backends. |
| 69 | +. Lines 59–88 — `connections` array, `mutex` declaration, `isValidTransition`. |
| 70 | +. Lines 95–113 — the simplest export (`db_connect`); read before the sqlite/verisimdb variants. |
| 71 | +. Lines 324–401 — `db_execute_sql` — full state-machine-guarded sqlite exec with JSON serialisation. |
| 72 | +. Lines 515–641 — `db_execute_vql` + `runCurlPost` — the two-phase lock pattern for blocking I/O. |
| 73 | +. Lines 842–872 — the standard cartridge-interface exports required by the BoJ loader. |
0 commit comments