cratefield_core/ports/
database.rs1use async_trait::async_trait;
5use sea_query::Value as SeaValue;
6
7#[derive(Debug, Clone)]
12pub struct Statement {
13 pub sql: String,
14 pub values: sea_query::Values,
15}
16
17impl Statement {
18 pub fn new(sql: impl Into<String>) -> Self {
20 Self {
21 sql: sql.into(),
22 values: sea_query::Values(Vec::new()),
23 }
24 }
25
26 pub fn with_values(sql: impl Into<String>, values: Vec<SeaValue>) -> Self {
28 Self {
29 sql: sql.into(),
30 values: sea_query::Values(values),
31 }
32 }
33
34 pub fn render(query: &impl sea_query::QueryStatementBuilder) -> Self {
38 let (sql, values) = query.build_any(&sea_query::SqliteQueryBuilder);
39 Self { sql, values }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct Rows {
46 pub rows: Vec<Row>,
47}
48
49impl Rows {
50 pub fn new(rows: Vec<Row>) -> Self {
51 Self { rows }
52 }
53
54 pub fn is_empty(&self) -> bool {
55 self.rows.is_empty()
56 }
57
58 pub fn len(&self) -> usize {
59 self.rows.len()
60 }
61
62 pub fn first(&self) -> Option<&Row> {
63 self.rows.first()
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct Row {
70 columns: Vec<(String, SeaValue)>,
71}
72
73impl Row {
74 pub fn new(columns: Vec<(String, SeaValue)>) -> Self {
75 Self { columns }
76 }
77
78 pub fn column_names(&self) -> impl Iterator<Item = &str> {
80 self.columns.iter().map(|(name, _)| name.as_str())
81 }
82
83 pub fn columns(&self) -> impl Iterator<Item = (&str, &SeaValue)> {
92 self.columns
93 .iter()
94 .map(|(name, value)| (name.as_str(), value))
95 }
96
97 pub fn get<T: TryFromValue>(&self, column: &str) -> Option<T> {
100 let value = self
101 .columns
102 .iter()
103 .find(|(name, _)| name == column)
104 .map(|(_, value)| value)?;
105 T::try_from_value(value)
106 }
107}
108
109pub trait TryFromValue: Sized {
111 fn try_from_value(value: &SeaValue) -> Option<Self>;
112}
113
114fn text(value: &SeaValue) -> Option<String> {
115 match value {
116 SeaValue::String(Some(s)) => Some((**s).clone()),
117 SeaValue::Char(Some(c)) => Some(c.to_string()),
118 _ => None,
119 }
120}
121
122macro_rules! impl_int {
123 ($($t:ty),* $(,)?) => {
124 $(
125 impl TryFromValue for $t {
126 fn try_from_value(value: &SeaValue) -> Option<Self> {
127 let i: i64 = match value {
128 SeaValue::TinyInt(Some(v)) => i64::from(*v),
129 SeaValue::SmallInt(Some(v)) => i64::from(*v),
130 SeaValue::Int(Some(v)) => i64::from(*v),
131 SeaValue::BigInt(Some(v)) => *v,
132 _ => return None,
133 };
134 <$t>::try_from(i).ok()
135 }
136 }
137 )*
138 };
139}
140
141impl_int!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
142
143impl TryFromValue for String {
144 fn try_from_value(value: &SeaValue) -> Option<Self> {
145 text(value)
146 }
147}
148
149impl TryFromValue for Vec<u8> {
152 fn try_from_value(value: &SeaValue) -> Option<Self> {
153 match value {
154 SeaValue::Bytes(Some(bytes)) => Some(bytes.as_ref().clone()),
155 _ => None,
156 }
157 }
158}
159
160impl TryFromValue for bool {
161 fn try_from_value(value: &SeaValue) -> Option<Self> {
162 match value {
163 SeaValue::Bool(Some(v)) => Some(*v),
164 SeaValue::Int(Some(v)) => Some(*v != 0),
165 _ => None,
166 }
167 }
168}
169
170impl TryFromValue for f64 {
171 fn try_from_value(value: &SeaValue) -> Option<Self> {
172 match value {
173 SeaValue::Float(Some(v)) => Some(f64::from(*v)),
174 SeaValue::Double(Some(v)) => Some(*v),
175 _ => None,
176 }
177 }
178}
179
180impl TryFromValue for Option<String> {
181 fn try_from_value(value: &SeaValue) -> Option<Self> {
182 match value {
185 SeaValue::String(inner) => Some(inner.as_deref().map(String::from)),
186 _ => None,
187 }
188 }
189}
190
191impl TryFromValue for SeaValue {
192 fn try_from_value(value: &SeaValue) -> Option<Self> {
193 Some(value.clone())
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
209pub enum DbError {
210 Execute(String),
211 Query(String),
212 Batch(String),
213}
214
215impl std::fmt::Display for DbError {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 let (label, message) = match self {
218 Self::Execute(message) => ("execute failed", message),
219 Self::Query(message) => ("query failed", message),
220 Self::Batch(message) => ("batch failed", message),
221 };
222 write!(f, "{label}: {}", crate::logging::scrub_text(message))
223 }
224}
225
226impl std::error::Error for DbError {}
227
228#[async_trait]
243pub trait Database: Send + Sync {
244 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError>;
245 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError>;
246 async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError>;
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn display_sanitizes_the_driver_message() {
255 let error = DbError::Execute(
259 "duplicate key value violates unique constraint \"subscribers_email_normalized_key\" \
260 DETAIL: Key (email_normalized)=(nick@example.com) already exists."
261 .to_owned(),
262 );
263 let text = error.to_string();
264 assert!(text.starts_with("execute failed: "), "{text}");
265 assert!(!text.contains('@'), "{text}");
266 assert!(!text.contains("nick"), "{text}");
267 assert!(text.contains("[subject_hash:"), "{text}");
268
269 let error = DbError::Execute(
271 "error connecting to postgres://venture:sup3r-s3cret@db.internal:5432/app".to_owned(),
272 );
273 let text = error.to_string();
274 assert!(!text.contains("sup3r-s3cret"), "{text}");
275 assert!(text.contains("postgres://[redacted]@db.internal"), "{text}");
276
277 assert_eq!(
278 DbError::Query("no such table: subscribers".to_owned()).to_string(),
279 "query failed: no such table: subscribers"
280 );
281 assert_eq!(
282 DbError::Batch("batch aborted".to_owned()).to_string(),
283 "batch failed: batch aborted"
284 );
285 }
286}