Skip to main content

helios_persistence/core/
backend.rs

1//! Backend abstraction for database drivers.
2//!
3//! This module defines the [`Backend`] trait, which provides a Diesel-inspired
4//! abstraction over different database backends. Each backend implements this
5//! trait to provide database-specific query building and execution.
6
7use std::fmt::Debug;
8
9use async_trait::async_trait;
10
11use crate::error::BackendError;
12
13/// Identifies the type of database backend.
14///
15/// This enum is used for runtime capability checks and query routing
16/// in composite storage configurations.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum BackendKind {
19    /// SQLite database (file-based or in-memory).
20    Sqlite,
21    /// PostgreSQL database.
22    Postgres,
23    /// Apache Cassandra (wide-column store).
24    Cassandra,
25    /// MongoDB (document store).
26    MongoDB,
27    /// Neo4j (graph database).
28    Neo4j,
29    /// Elasticsearch (search engine).
30    Elasticsearch,
31    /// AWS S3 (object storage).
32    S3,
33    /// Custom or unknown backend.
34    Custom(&'static str),
35}
36
37impl std::fmt::Display for BackendKind {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            BackendKind::Sqlite => write!(f, "sqlite"),
41            BackendKind::Postgres => write!(f, "postgres"),
42            BackendKind::Cassandra => write!(f, "cassandra"),
43            BackendKind::MongoDB => write!(f, "mongodb"),
44            BackendKind::Neo4j => write!(f, "neo4j"),
45            BackendKind::Elasticsearch => write!(f, "elasticsearch"),
46            BackendKind::S3 => write!(f, "s3"),
47            BackendKind::Custom(name) => write!(f, "{}", name),
48        }
49    }
50}
51
52/// Capabilities that a backend may support.
53///
54/// Used for runtime capability discovery and query routing.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum BackendCapability {
57    /// Basic CRUD operations.
58    Crud,
59    /// Resource versioning (vread).
60    Versioning,
61    /// Instance-level history.
62    InstanceHistory,
63    /// Type-level history.
64    TypeHistory,
65    /// System-level history.
66    SystemHistory,
67    /// Basic search with token/string parameters.
68    BasicSearch,
69    /// Date range search.
70    DateSearch,
71    /// Quantity search with units.
72    QuantitySearch,
73    /// Reference search.
74    ReferenceSearch,
75    /// Chained search parameters.
76    ChainedSearch,
77    /// Reverse chaining (_has).
78    ReverseChaining,
79    /// _include support.
80    Include,
81    /// _revinclude support.
82    Revinclude,
83    /// Full-text search (_text, _content, :text).
84    FullTextSearch,
85    /// Terminology operations (:above, :below, :in, :not-in).
86    TerminologySearch,
87    /// ACID transactions.
88    Transactions,
89    /// Optimistic locking (If-Match).
90    OptimisticLocking,
91    /// Pessimistic locking.
92    PessimisticLocking,
93    /// Cursor-based pagination.
94    CursorPagination,
95    /// Offset-based pagination.
96    OffsetPagination,
97    /// Sorting results.
98    Sorting,
99    /// Bulk export operations.
100    BulkExport,
101    /// Bulk import operations.
102    BulkImport,
103    /// Shared schema multitenancy.
104    SharedSchema,
105    /// Schema-per-tenant multitenancy.
106    SchemaPerTenant,
107    /// Database-per-tenant multitenancy.
108    DatabasePerTenant,
109}
110
111impl std::fmt::Display for BackendCapability {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        let name = match self {
114            BackendCapability::Crud => "crud",
115            BackendCapability::Versioning => "versioning",
116            BackendCapability::InstanceHistory => "instance-history",
117            BackendCapability::TypeHistory => "type-history",
118            BackendCapability::SystemHistory => "system-history",
119            BackendCapability::BasicSearch => "basic-search",
120            BackendCapability::DateSearch => "date-search",
121            BackendCapability::QuantitySearch => "quantity-search",
122            BackendCapability::ReferenceSearch => "reference-search",
123            BackendCapability::ChainedSearch => "chained-search",
124            BackendCapability::ReverseChaining => "reverse-chaining",
125            BackendCapability::Include => "include",
126            BackendCapability::Revinclude => "revinclude",
127            BackendCapability::FullTextSearch => "full-text-search",
128            BackendCapability::TerminologySearch => "terminology-search",
129            BackendCapability::Transactions => "transactions",
130            BackendCapability::OptimisticLocking => "optimistic-locking",
131            BackendCapability::PessimisticLocking => "pessimistic-locking",
132            BackendCapability::CursorPagination => "cursor-pagination",
133            BackendCapability::OffsetPagination => "offset-pagination",
134            BackendCapability::Sorting => "sorting",
135            BackendCapability::BulkExport => "bulk-export",
136            BackendCapability::BulkImport => "bulk-import",
137            BackendCapability::SharedSchema => "shared-schema",
138            BackendCapability::SchemaPerTenant => "schema-per-tenant",
139            BackendCapability::DatabasePerTenant => "database-per-tenant",
140        };
141        write!(f, "{}", name)
142    }
143}
144
145/// Configuration for a database backend.
146#[derive(Debug, Clone)]
147pub struct BackendConfig {
148    /// Connection string or URL.
149    pub connection_string: String,
150    /// Maximum number of connections in the pool.
151    pub max_connections: u32,
152    /// Minimum number of idle connections.
153    pub min_connections: u32,
154    /// Connection timeout in milliseconds.
155    pub connect_timeout_ms: u64,
156    /// Idle connection timeout in milliseconds.
157    pub idle_timeout_ms: Option<u64>,
158    /// Maximum connection lifetime in milliseconds.
159    pub max_lifetime_ms: Option<u64>,
160}
161
162impl Default for BackendConfig {
163    fn default() -> Self {
164        Self {
165            connection_string: String::new(),
166            max_connections: 10,
167            min_connections: 1,
168            connect_timeout_ms: 5000,
169            idle_timeout_ms: Some(600_000),   // 10 minutes
170            max_lifetime_ms: Some(1_800_000), // 30 minutes
171        }
172    }
173}
174
175impl BackendConfig {
176    /// Creates a new configuration with the given connection string.
177    pub fn new(connection_string: impl Into<String>) -> Self {
178        Self {
179            connection_string: connection_string.into(),
180            ..Default::default()
181        }
182    }
183
184    /// Sets the maximum number of connections.
185    pub fn with_max_connections(mut self, max: u32) -> Self {
186        self.max_connections = max;
187        self
188    }
189
190    /// Sets the connection timeout.
191    pub fn with_connect_timeout_ms(mut self, timeout: u64) -> Self {
192        self.connect_timeout_ms = timeout;
193        self
194    }
195}
196
197/// A database backend that can execute storage operations.
198///
199/// This trait is inspired by Diesel's `Backend` trait, providing a common
200/// abstraction over different database drivers. Each backend implementation
201/// provides its own connection type and query builder.
202///
203/// # Design
204///
205/// The `Backend` trait is designed to be object-safe where possible, allowing
206/// for dynamic dispatch in composite storage scenarios. However, some operations
207/// require associated types for type safety.
208///
209/// # Example
210///
211/// ```ignore
212/// use helios_persistence::core::{Backend, BackendKind, BackendCapability};
213///
214/// // Check backend capabilities at runtime
215/// if backend.supports(BackendCapability::ChainedSearch) {
216///     // Use chained search
217/// } else {
218///     // Fall back to multiple queries
219/// }
220/// ```
221#[async_trait]
222pub trait Backend: Send + Sync + Debug {
223    /// The type of raw connection used by this backend.
224    type Connection: Send;
225
226    /// Returns the kind of backend.
227    fn kind(&self) -> BackendKind;
228
229    /// Returns a human-readable name for this backend.
230    fn name(&self) -> &'static str;
231
232    /// Checks if this backend supports the given capability.
233    fn supports(&self, capability: BackendCapability) -> bool;
234
235    /// Returns all capabilities supported by this backend.
236    fn capabilities(&self) -> Vec<BackendCapability>;
237
238    /// Acquires a connection from the pool.
239    async fn acquire(&self) -> Result<Self::Connection, BackendError>;
240
241    /// Returns the connection back to the pool.
242    async fn release(&self, conn: Self::Connection);
243
244    /// Checks if the backend is healthy and accepting connections.
245    async fn health_check(&self) -> Result<(), BackendError>;
246
247    /// Initializes the database schema if needed.
248    async fn initialize(&self) -> Result<(), BackendError>;
249
250    /// Runs any pending migrations.
251    async fn migrate(&self) -> Result<(), BackendError>;
252}
253
254/// Extension trait for backends that support connection pooling statistics.
255pub trait BackendPoolStats {
256    /// Returns the current number of active connections.
257    fn active_connections(&self) -> u32;
258
259    /// Returns the current number of idle connections.
260    fn idle_connections(&self) -> u32;
261
262    /// Returns the maximum pool size.
263    fn max_connections(&self) -> u32;
264
265    /// Returns the number of connections waiting to be acquired.
266    fn pending_connections(&self) -> u32;
267}
268
269/// Marker trait for backends that support ACID transactions.
270pub trait TransactionalBackend: Backend {}
271
272/// Marker trait for backends that support full-text search.
273pub trait FullTextBackend: Backend {}
274
275/// Marker trait for backends optimized for graph queries.
276pub trait GraphBackend: Backend {}
277
278/// Marker trait for backends optimized for time-series data.
279pub trait TimeSeriesBackend: Backend {}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_backend_kind_display() {
287        assert_eq!(BackendKind::Sqlite.to_string(), "sqlite");
288        assert_eq!(BackendKind::Postgres.to_string(), "postgres");
289        assert_eq!(BackendKind::Custom("custom-db").to_string(), "custom-db");
290    }
291
292    #[test]
293    fn test_backend_capability_display() {
294        assert_eq!(BackendCapability::Crud.to_string(), "crud");
295        assert_eq!(
296            BackendCapability::ChainedSearch.to_string(),
297            "chained-search"
298        );
299        assert_eq!(
300            BackendCapability::FullTextSearch.to_string(),
301            "full-text-search"
302        );
303    }
304
305    #[test]
306    fn test_backend_config_default() {
307        let config = BackendConfig::default();
308        assert_eq!(config.max_connections, 10);
309        assert_eq!(config.min_connections, 1);
310        assert_eq!(config.connect_timeout_ms, 5000);
311    }
312
313    #[test]
314    fn test_backend_config_builder() {
315        let config = BackendConfig::new("postgres://localhost/db")
316            .with_max_connections(20)
317            .with_connect_timeout_ms(10000);
318
319        assert_eq!(config.connection_string, "postgres://localhost/db");
320        assert_eq!(config.max_connections, 20);
321        assert_eq!(config.connect_timeout_ms, 10000);
322    }
323}