rskit_database/
database.rs1use std::collections::VecDeque;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Instant;
6
7use async_trait::async_trait;
8use parking_lot::Mutex;
9use rskit_bootstrap::{Component, Health};
10use rskit_errors::{AppError, AppResult, ErrorCode};
11use serde::{Deserialize, Serialize};
12
13use crate::config::{DatabaseConfig, MemoryDatabaseConfig};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct DatabaseQuery {
18 pub statement: String,
20 #[serde(default)]
22 pub parameters: Vec<serde_json::Value>,
23}
24
25impl DatabaseQuery {
26 #[must_use]
28 pub fn new(statement: impl Into<String>) -> Self {
29 Self {
30 statement: statement.into(),
31 parameters: Vec::new(),
32 }
33 }
34
35 #[must_use]
37 pub fn with_parameter(mut self, value: impl Into<serde_json::Value>) -> Self {
38 self.parameters.push(value.into());
39 self
40 }
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct DatabaseResult {
46 pub rows_affected: u64,
48}
49
50#[async_trait]
52pub trait DatabaseTransaction: Send + Sync {
53 async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult>;
55 async fn commit(self: Box<Self>) -> AppResult<()>;
57 async fn rollback(self: Box<Self>) -> AppResult<()>;
59}
60
61#[async_trait]
63pub trait DatabaseClient: Send + Sync {
64 async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult>;
66 async fn begin(&self) -> AppResult<Box<dyn DatabaseTransaction>>;
68 async fn ping(&self) -> AppResult<()>;
70}
71
72#[derive(Debug)]
74pub struct InMemoryDatabase {
75 config: MemoryDatabaseConfig,
76 connected: AtomicBool,
77 history: Mutex<VecDeque<DatabaseQuery>>,
78}
79
80impl InMemoryDatabase {
81 #[must_use]
83 pub fn new(config: MemoryDatabaseConfig) -> Self {
84 Self {
85 config,
86 connected: AtomicBool::new(true),
87 history: Mutex::new(VecDeque::new()),
88 }
89 }
90
91 #[must_use]
93 pub fn recorded_queries(&self) -> Vec<DatabaseQuery> {
94 self.history.lock().iter().cloned().collect()
95 }
96
97 fn record(&self, query: DatabaseQuery) {
98 if self.config.statement_history == 0 {
99 return;
100 }
101 let mut history = self.history.lock();
102 if history.len() == self.config.statement_history {
103 history.pop_front();
104 }
105 history.push_back(query);
106 }
107}
108
109impl Default for InMemoryDatabase {
110 fn default() -> Self {
111 Self::new(MemoryDatabaseConfig::default())
112 }
113}
114
115#[async_trait]
116impl DatabaseClient for InMemoryDatabase {
117 async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult> {
118 if !self.connected.load(Ordering::SeqCst) {
119 return Err(AppError::new(
120 ErrorCode::ConnectionFailed,
121 "in-memory database is stopped",
122 ));
123 }
124 if query.statement.trim().is_empty() {
125 return Err(AppError::new(
126 ErrorCode::InvalidInput,
127 "database query statement is required",
128 ));
129 }
130 self.record(query);
131 Ok(DatabaseResult { rows_affected: 1 })
132 }
133
134 async fn begin(&self) -> AppResult<Box<dyn DatabaseTransaction>> {
135 self.ping().await?;
136 Ok(Box::new(InMemoryTransaction {
137 started_at: Instant::now(),
138 queries: Mutex::new(Vec::new()),
139 committed: AtomicBool::new(false),
140 rolled_back: AtomicBool::new(false),
141 }))
142 }
143
144 async fn ping(&self) -> AppResult<()> {
145 if self.connected.load(Ordering::SeqCst) {
146 Ok(())
147 } else {
148 Err(AppError::new(
149 ErrorCode::ConnectionFailed,
150 "in-memory database is stopped",
151 ))
152 }
153 }
154}
155
156#[async_trait]
157impl Component for InMemoryDatabase {
158 fn name(&self) -> &str {
159 "database"
160 }
161
162 async fn start(&self) -> AppResult<()> {
163 self.connected.store(true, Ordering::SeqCst);
164 Ok(())
165 }
166
167 async fn stop(&self) -> AppResult<()> {
168 self.connected.store(false, Ordering::SeqCst);
169 Ok(())
170 }
171
172 fn health(&self) -> Health {
173 if self.connected.load(Ordering::SeqCst) {
174 Health::healthy("database")
175 } else {
176 Health::unhealthy("database", "backend is stopped")
177 }
178 }
179}
180
181#[derive(Debug)]
182struct InMemoryTransaction {
183 started_at: Instant,
184 queries: Mutex<Vec<DatabaseQuery>>,
185 committed: AtomicBool,
186 rolled_back: AtomicBool,
187}
188
189#[async_trait]
190impl DatabaseTransaction for InMemoryTransaction {
191 async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult> {
192 if query.statement.trim().is_empty() {
193 return Err(AppError::new(
194 ErrorCode::InvalidInput,
195 "database query statement is required",
196 ));
197 }
198 self.queries.lock().push(query);
199 Ok(DatabaseResult { rows_affected: 1 })
200 }
201
202 async fn commit(self: Box<Self>) -> AppResult<()> {
203 if self.rolled_back.load(Ordering::SeqCst) {
204 return Err(AppError::new(
205 ErrorCode::InvalidInput,
206 "database transaction was already rolled back",
207 ));
208 }
209 self.committed.store(true, Ordering::SeqCst);
210 let _elapsed = self.started_at.elapsed();
211 Ok(())
212 }
213
214 async fn rollback(self: Box<Self>) -> AppResult<()> {
215 if self.committed.load(Ordering::SeqCst) {
216 return Err(AppError::new(
217 ErrorCode::InvalidInput,
218 "database transaction was already committed",
219 ));
220 }
221 self.rolled_back.store(true, Ordering::SeqCst);
222 Ok(())
223 }
224}
225
226pub(crate) fn memory_from_config(config: &DatabaseConfig) -> InMemoryDatabase {
227 InMemoryDatabase::new(config.memory.clone())
228}
229
230#[cfg(test)]
231mod tests {
232 use std::sync::atomic::AtomicBool;
233
234 use parking_lot::Mutex;
235 use rskit_bootstrap::Component;
236
237 use super::*;
238
239 #[tokio::test]
240 async fn in_memory_database_records_bounded_history_and_component_state() {
241 let database = InMemoryDatabase::new(MemoryDatabaseConfig {
242 name: "test".to_owned(),
243 statement_history: 1,
244 });
245
246 assert_eq!(database.name(), "database");
247 database
248 .execute(DatabaseQuery::new("select 1").with_parameter(1))
249 .await
250 .unwrap();
251 database
252 .execute(DatabaseQuery::new("select 2"))
253 .await
254 .unwrap();
255 let recorded = database.recorded_queries();
256 assert_eq!(recorded.len(), 1);
257 assert_eq!(recorded[0].statement, "select 2");
258 assert!(database.health().is_healthy());
259
260 database.stop().await.unwrap();
261 assert_eq!(
262 database.ping().await.unwrap_err().code(),
263 ErrorCode::ConnectionFailed
264 );
265 assert_eq!(
266 database
267 .execute(DatabaseQuery::new("select 3"))
268 .await
269 .unwrap_err()
270 .code(),
271 ErrorCode::ConnectionFailed
272 );
273 assert!(!database.health().is_healthy());
274
275 database.start().await.unwrap();
276 assert_eq!(
277 database
278 .execute(DatabaseQuery::new(" "))
279 .await
280 .unwrap_err()
281 .code(),
282 ErrorCode::InvalidInput
283 );
284 }
285
286 #[tokio::test]
287 async fn in_memory_transactions_validate_execute_and_guard_terminal_state() {
288 let tx = InMemoryDatabase::default().begin().await.unwrap();
289 assert_eq!(
290 tx.execute(DatabaseQuery::new(" "))
291 .await
292 .unwrap_err()
293 .code(),
294 ErrorCode::InvalidInput
295 );
296 assert_eq!(
297 tx.execute(DatabaseQuery::new("insert"))
298 .await
299 .unwrap()
300 .rows_affected,
301 1
302 );
303 tx.commit().await.unwrap();
304
305 let committed = Box::new(InMemoryTransaction {
306 started_at: Instant::now(),
307 queries: Mutex::new(Vec::new()),
308 committed: AtomicBool::new(true),
309 rolled_back: AtomicBool::new(false),
310 });
311 assert_eq!(
312 committed.rollback().await.unwrap_err().code(),
313 ErrorCode::InvalidInput
314 );
315
316 let rolled_back = Box::new(InMemoryTransaction {
317 started_at: Instant::now(),
318 queries: Mutex::new(Vec::new()),
319 committed: AtomicBool::new(false),
320 rolled_back: AtomicBool::new(true),
321 });
322 assert_eq!(
323 rolled_back.commit().await.unwrap_err().code(),
324 ErrorCode::InvalidInput
325 );
326
327 InMemoryDatabase::default()
328 .begin()
329 .await
330 .unwrap()
331 .rollback()
332 .await
333 .unwrap();
334 }
335
336 #[test]
337 fn zero_statement_history_disables_recording() {
338 let database = InMemoryDatabase::new(MemoryDatabaseConfig {
339 name: "test".to_owned(),
340 statement_history: 0,
341 });
342
343 database.record(DatabaseQuery::new("select"));
344
345 assert!(database.recorded_queries().is_empty());
346 }
347}