khive_storage/sql.rs
1//! SQL access capability traits.
2
3use std::any::Any;
4use std::future::Future;
5use std::pin::Pin;
6
7use async_trait::async_trait;
8
9use crate::types::{SqlRow, SqlStatement, SqlValue, StorageResult};
10
11/// A boxed future, borrowing from the `&mut dyn SqlWriter` an
12/// [`AtomicUnitOp`] is called with (see [`SqlAccess::atomic_unit`]).
13pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
14
15/// A caller-supplied unit of work to run as ONE atomic operation via
16/// [`SqlAccess::atomic_unit`] (ADR-067 Component A, Fork C slice 2).
17///
18/// `op` receives a live `&mut dyn SqlWriter` already inside an open write
19/// transaction — it must issue DML only (no bare `BEGIN`/`COMMIT`/
20/// `ROLLBACK`; the caller-visible transaction boundary is owned entirely by
21/// `atomic_unit`, exactly like the existing `execute_batch` contract) — and
22/// returns its result type-erased via `Box<dyn Any + Send>` so this trait
23/// method stays object-safe (no method-level generics on a trait used as
24/// `dyn SqlAccess`). Callers downcast the returned box back to their own
25/// concrete outcome type.
26pub type AtomicUnitOp = Box<
27 dyn for<'w> FnOnce(&'w mut dyn SqlWriter) -> BoxFuture<'w, StorageResult<Box<dyn Any + Send>>>
28 + Send,
29>;
30
31/// Read-capable SQL connection.
32#[async_trait]
33pub trait SqlReader: Send + 'static {
34 /// Execute `statement` and return the first row, or `None` if the result set is empty.
35 async fn query_row(&mut self, statement: SqlStatement) -> StorageResult<Option<SqlRow>>;
36 /// Execute `statement` and return all rows.
37 async fn query_all(&mut self, statement: SqlStatement) -> StorageResult<Vec<SqlRow>>;
38 /// Execute `statement` and return the first column of the first row as a scalar.
39 async fn query_scalar(&mut self, statement: SqlStatement) -> StorageResult<Option<SqlValue>>;
40 /// Run `EXPLAIN QUERY PLAN` for `statement` and return the plan rows.
41 async fn explain(&mut self, statement: SqlStatement) -> StorageResult<Vec<SqlRow>>;
42}
43
44/// Write-capable SQL connection (extends `SqlReader`).
45#[async_trait]
46pub trait SqlWriter: SqlReader + Send + 'static {
47 /// Execute a single DML statement and return the number of rows affected.
48 async fn execute(&mut self, statement: SqlStatement) -> StorageResult<u64>;
49 /// Execute multiple DML statements and return the total rows affected.
50 async fn execute_batch(&mut self, statements: Vec<SqlStatement>) -> StorageResult<u64>;
51 /// Execute a raw SQL script (no parameters; used for migrations).
52 async fn execute_script(&mut self, script: String) -> StorageResult<()>;
53
54 /// Execute a raw SQL script that MUST run outside any open transaction
55 /// (ADR-067 Component A, Fork C slice 2) — e.g.
56 /// `VACUUM`, which SQLite rejects if issued inside `BEGIN`/`COMMIT`.
57 ///
58 /// Default implementation delegates to [`Self::execute_script`]: every
59 /// writer implementation in this codebase except khive-db's
60 /// write-queue-routed `SqliteWriter` already runs `execute_script`
61 /// transaction-free (a plain connection call, or already inside a
62 /// caller-managed transaction where a top-level statement would be
63 /// invalid regardless of which method is called). `SqliteWriter`
64 /// overrides this to route around its writer task's per-request `BEGIN
65 /// IMMEDIATE` specifically for this call, while still serializing
66 /// through the single writer owner.
67 async fn execute_script_top_level(&mut self, script: String) -> StorageResult<()> {
68 self.execute_script(script).await
69 }
70}
71
72/// Base SQL access capability.
73#[async_trait]
74pub trait SqlAccess: Send + Sync + 'static {
75 /// Acquire a read-only connection from the pool.
76 async fn reader(&self) -> StorageResult<Box<dyn SqlReader>>;
77 /// Acquire a read-write connection from the pool.
78 async fn writer(&self) -> StorageResult<Box<dyn SqlWriter>>;
79
80 /// Run `op` as ONE atomic unit of work (ADR-067 Component A, Fork C
81 /// slice 2).
82 ///
83 /// Where a single-writer task is active (file-backed pool,
84 /// `KHIVE_WRITE_QUEUE=1`), `op` runs inside that task's one write
85 /// transaction for this request — no separate connection is opened, so
86 /// this call cannot compete with the writer task for SQLite's write
87 /// lock. Where no writer task applies (flag off, no runtime, or an
88 /// in-memory pool), `op` runs under a manual
89 /// `BEGIN IMMEDIATE`/`COMMIT`/`ROLLBACK` on a writer handle exactly like
90 /// calling [`Self::writer`] and driving the statements by hand — the
91 /// pre-ADR-067 behavior, preserved byte-for-byte on this path.
92 ///
93 /// **The atomic-unit suspend-free invariant (normative for every
94 /// caller):** `op`'s future must complete on its **first poll** — it may
95 /// issue only synchronous DML against the `&mut dyn SqlWriter` it is
96 /// handed and must never reach a real suspension point (no embedding
97 /// computation, no ANN warming, no service or channel `await`, no
98 /// network round-trip). On the single-writer path this is enforced at
99 /// runtime: the writer task drives `op` through a single-poll driver and
100 /// returns a typed error the instant the future is `Pending`, so a
101 /// violation fails loudly rather than corrupting state. On the flag-off
102 /// path (no writer task active) a suspending `op` would currently
103 /// *succeed* — that path drives `op` as an ordinary `.await` under a
104 /// manual transaction — so the invariant is a correctness contract this
105 /// trait asks every caller to uphold, not something the type system or
106 /// every code path enforces. Callers must not rely on the flag-off
107 /// path's tolerance; behavior must be identical (synchronous DML only)
108 /// regardless of whether the single-writer flag is on.
109 async fn atomic_unit(&self, op: AtomicUnitOp) -> StorageResult<Box<dyn Any + Send>>;
110}