Skip to main content

cratefield_core/ports/
database.rs

1//! The `Database` port: engine-agnostic statements and rows over
2//! sea-query-rendered SQL (ADR 0004).
3
4use async_trait::async_trait;
5use sea_query::Value as SeaValue;
6use thiserror::Error;
7
8/// A rendered SQL statement: `(sql, values)` with `?` placeholders, produced
9/// by rendering a sea-query query for a dialect. Modules build queries with
10/// sea-query and render through the helpers on this type (or let adapters do
11/// it); adapters bind `values` positionally.
12#[derive(Debug, Clone)]
13pub struct Statement {
14    pub sql: String,
15    pub values: sea_query::Values,
16}
17
18impl Statement {
19    /// A parameterless statement (`SELECT 1`, DDL).
20    pub fn new(sql: impl Into<String>) -> Self {
21        Self {
22            sql: sql.into(),
23            values: sea_query::Values(Vec::new()),
24        }
25    }
26
27    /// A statement with positional `?` values.
28    pub fn with_values(sql: impl Into<String>, values: Vec<SeaValue>) -> Self {
29        Self {
30            sql: sql.into(),
31            values: sea_query::Values(values),
32        }
33    }
34
35    /// Renders a sea-query statement (select/insert/update/delete) with the
36    /// SQLite dialect. The portable subset renders identically for D1,
37    /// rusqlite and (later) Postgres (ADR 0004).
38    pub fn render(query: &impl sea_query::QueryStatementBuilder) -> Self {
39        let (sql, values) = query.build_any(&sea_query::SqliteQueryBuilder);
40        Self { sql, values }
41    }
42}
43
44/// A small owned row model. No engine types leak past this point.
45#[derive(Debug, Clone)]
46pub struct Rows {
47    pub rows: Vec<Row>,
48}
49
50impl Rows {
51    pub fn new(rows: Vec<Row>) -> Self {
52        Self { rows }
53    }
54
55    pub fn is_empty(&self) -> bool {
56        self.rows.is_empty()
57    }
58
59    pub fn len(&self) -> usize {
60        self.rows.len()
61    }
62
63    pub fn first(&self) -> Option<&Row> {
64        self.rows.first()
65    }
66}
67
68/// One result row: ordered `(column name, value)` pairs.
69#[derive(Debug, Clone)]
70pub struct Row {
71    columns: Vec<(String, SeaValue)>,
72}
73
74impl Row {
75    pub fn new(columns: Vec<(String, SeaValue)>) -> Self {
76        Self { columns }
77    }
78
79    /// Column names in result order.
80    pub fn column_names(&self) -> impl Iterator<Item = &str> {
81        self.columns.iter().map(|(name, _)| name.as_str())
82    }
83
84    /// Typed column access. `None` when the column is missing, `NULL`, or
85    /// not representable as `T`.
86    pub fn get<T: TryFromValue>(&self, column: &str) -> Option<T> {
87        let value = self
88            .columns
89            .iter()
90            .find(|(name, _)| name == column)
91            .map(|(_, value)| value)?;
92        T::try_from_value(value)
93    }
94}
95
96/// Conversion from a sea-query [`SeaValue`] for typed row access.
97pub trait TryFromValue: Sized {
98    fn try_from_value(value: &SeaValue) -> Option<Self>;
99}
100
101fn text(value: &SeaValue) -> Option<String> {
102    match value {
103        SeaValue::String(Some(s)) => Some((**s).clone()),
104        SeaValue::Char(Some(c)) => Some(c.to_string()),
105        _ => None,
106    }
107}
108
109macro_rules! impl_int {
110    ($($t:ty),* $(,)?) => {
111        $(
112            impl TryFromValue for $t {
113                fn try_from_value(value: &SeaValue) -> Option<Self> {
114                    let i: i64 = match value {
115                        SeaValue::TinyInt(Some(v)) => i64::from(*v),
116                        SeaValue::SmallInt(Some(v)) => i64::from(*v),
117                        SeaValue::Int(Some(v)) => i64::from(*v),
118                        SeaValue::BigInt(Some(v)) => *v,
119                        _ => return None,
120                    };
121                    <$t>::try_from(i).ok()
122                }
123            }
124        )*
125    };
126}
127
128impl_int!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
129
130impl TryFromValue for String {
131    fn try_from_value(value: &SeaValue) -> Option<Self> {
132        text(value)
133    }
134}
135
136/// Blob columns. A `Database` that cannot return bytes cannot hold a
137/// ciphertext, a wrapped key or a nonce (issue #39).
138impl TryFromValue for Vec<u8> {
139    fn try_from_value(value: &SeaValue) -> Option<Self> {
140        match value {
141            SeaValue::Bytes(Some(bytes)) => Some(bytes.as_ref().clone()),
142            _ => None,
143        }
144    }
145}
146
147impl TryFromValue for bool {
148    fn try_from_value(value: &SeaValue) -> Option<Self> {
149        match value {
150            SeaValue::Bool(Some(v)) => Some(*v),
151            SeaValue::Int(Some(v)) => Some(*v != 0),
152            _ => None,
153        }
154    }
155}
156
157impl TryFromValue for f64 {
158    fn try_from_value(value: &SeaValue) -> Option<Self> {
159        match value {
160            SeaValue::Float(Some(v)) => Some(f64::from(*v)),
161            SeaValue::Double(Some(v)) => Some(*v),
162            _ => None,
163        }
164    }
165}
166
167impl TryFromValue for Option<String> {
168    fn try_from_value(value: &SeaValue) -> Option<Self> {
169        // In sea-query 0.32 a SQL NULL is the variant with `None` inside
170        // (e.g. `Value::String(None)`); a type mismatch is our `None`.
171        match value {
172            SeaValue::String(inner) => Some(inner.as_deref().map(String::from)),
173            _ => None,
174        }
175    }
176}
177
178impl TryFromValue for SeaValue {
179    fn try_from_value(value: &SeaValue) -> Option<Self> {
180        Some(value.clone())
181    }
182}
183
184/// Database failures, sanitized for logs and problem details.
185#[derive(Debug, Clone, PartialEq, Eq, Error)]
186pub enum DbError {
187    #[error("execute failed: {0}")]
188    Execute(String),
189    #[error("query failed: {0}")]
190    Query(String),
191    #[error("batch failed: {0}")]
192    Batch(String),
193}
194
195/// Execute statements against the venture database. Implementations: D1
196/// (Workers), rusqlite (tests, self-hosted), Postgres (phase 3).
197///
198/// `batch` runs all statements in one unit of work where the engine supports
199/// it (a transaction on SQLite and D1's atomic batch); documented per
200/// adapter.
201#[async_trait]
202pub trait Database: Send + Sync {
203    async fn execute(&self, stmt: &Statement) -> Result<u64, DbError>;
204    async fn query(&self, stmt: &Statement) -> Result<Rows, DbError>;
205    async fn batch(&self, stmts: &[Statement]) -> Result<(), DbError>;
206}