Skip to main content

rskit_database/
database.rs

1//! Vendor-neutral database client contracts and the in-memory backend.
2
3use 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/// Backend-neutral database statement.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct DatabaseQuery {
18    /// Statement text in the backend's query language.
19    pub statement: String,
20    /// Positional parameters encoded as backend-neutral JSON values.
21    #[serde(default)]
22    pub parameters: Vec<serde_json::Value>,
23}
24
25impl DatabaseQuery {
26    /// Create a query without positional parameters.
27    #[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    /// Add one positional parameter.
36    #[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/// Backend-neutral database execution result.
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct DatabaseResult {
46    /// Rows affected by the statement, when the backend reports it.
47    pub rows_affected: u64,
48}
49
50/// Active database transaction.
51#[async_trait]
52pub trait DatabaseTransaction: Send + Sync {
53    /// Execute a statement inside the transaction.
54    async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult>;
55    /// Commit the transaction.
56    async fn commit(self: Box<Self>) -> AppResult<()>;
57    /// Roll back the transaction.
58    async fn rollback(self: Box<Self>) -> AppResult<()>;
59}
60
61/// Vendor-neutral database client.
62#[async_trait]
63pub trait DatabaseClient: Send + Sync {
64    /// Execute a statement.
65    async fn execute(&self, query: DatabaseQuery) -> AppResult<DatabaseResult>;
66    /// Begin a transaction.
67    async fn begin(&self) -> AppResult<Box<dyn DatabaseTransaction>>;
68    /// Validate that the backend is usable.
69    async fn ping(&self) -> AppResult<()>;
70}
71
72/// In-memory database backend for local development and tests.
73#[derive(Debug)]
74pub struct InMemoryDatabase {
75    config: MemoryDatabaseConfig,
76    connected: AtomicBool,
77    history: Mutex<VecDeque<DatabaseQuery>>,
78}
79
80impl InMemoryDatabase {
81    /// Create an in-memory database backend.
82    #[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    /// Return recorded statements retained by this backend.
92    #[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}