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    /// Synchronous Bulk Data Submit ingestion (`BulkSubmitProvider`).
102    BulkSubmitIngest,
103    /// Full `$bulk-submit` REST worker/job-store support.
104    BulkSubmitRestWorker,
105    /// Shared schema multitenancy.
106    SharedSchema,
107    /// Schema-per-tenant multitenancy.
108    SchemaPerTenant,
109    /// Database-per-tenant multitenancy.
110    DatabasePerTenant,
111    /// Backend can compile ViewDefinitions to SQL and run them in-DB (no in-process FHIRPath eval).
112    InDbSofRunner,
113    /// Backend supports raw SQL queries via `$sql-query-run` (Postgres, SQLite only).
114    RawSqlQuery,
115}
116
117impl std::fmt::Display for BackendCapability {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        let name = match self {
120            BackendCapability::Crud => "crud",
121            BackendCapability::Versioning => "versioning",
122            BackendCapability::InstanceHistory => "instance-history",
123            BackendCapability::TypeHistory => "type-history",
124            BackendCapability::SystemHistory => "system-history",
125            BackendCapability::BasicSearch => "basic-search",
126            BackendCapability::DateSearch => "date-search",
127            BackendCapability::QuantitySearch => "quantity-search",
128            BackendCapability::ReferenceSearch => "reference-search",
129            BackendCapability::ChainedSearch => "chained-search",
130            BackendCapability::ReverseChaining => "reverse-chaining",
131            BackendCapability::Include => "include",
132            BackendCapability::Revinclude => "revinclude",
133            BackendCapability::FullTextSearch => "full-text-search",
134            BackendCapability::TerminologySearch => "terminology-search",
135            BackendCapability::Transactions => "transactions",
136            BackendCapability::OptimisticLocking => "optimistic-locking",
137            BackendCapability::PessimisticLocking => "pessimistic-locking",
138            BackendCapability::CursorPagination => "cursor-pagination",
139            BackendCapability::OffsetPagination => "offset-pagination",
140            BackendCapability::Sorting => "sorting",
141            BackendCapability::BulkExport => "bulk-export",
142            BackendCapability::BulkSubmitIngest => "bulk-submit-ingest",
143            BackendCapability::BulkSubmitRestWorker => "bulk-submit-rest-worker",
144            BackendCapability::SharedSchema => "shared-schema",
145            BackendCapability::SchemaPerTenant => "schema-per-tenant",
146            BackendCapability::DatabasePerTenant => "database-per-tenant",
147            BackendCapability::InDbSofRunner => "indb-sof-runner",
148            BackendCapability::RawSqlQuery => "raw-sql-query",
149        };
150        write!(f, "{}", name)
151    }
152}
153
154/// Configuration for a database backend.
155#[derive(Debug, Clone)]
156pub struct BackendConfig {
157    /// Connection string or URL.
158    pub connection_string: String,
159    /// Maximum number of connections in the pool.
160    pub max_connections: u32,
161    /// Minimum number of idle connections.
162    pub min_connections: u32,
163    /// Connection timeout in milliseconds.
164    pub connect_timeout_ms: u64,
165    /// Idle connection timeout in milliseconds.
166    pub idle_timeout_ms: Option<u64>,
167    /// Maximum connection lifetime in milliseconds.
168    pub max_lifetime_ms: Option<u64>,
169}
170
171impl Default for BackendConfig {
172    fn default() -> Self {
173        Self {
174            connection_string: String::new(),
175            max_connections: 10,
176            min_connections: 1,
177            connect_timeout_ms: 5000,
178            idle_timeout_ms: Some(600_000),   // 10 minutes
179            max_lifetime_ms: Some(1_800_000), // 30 minutes
180        }
181    }
182}
183
184impl BackendConfig {
185    /// Creates a new configuration with the given connection string.
186    pub fn new(connection_string: impl Into<String>) -> Self {
187        Self {
188            connection_string: connection_string.into(),
189            ..Default::default()
190        }
191    }
192
193    /// Sets the maximum number of connections.
194    pub fn with_max_connections(mut self, max: u32) -> Self {
195        self.max_connections = max;
196        self
197    }
198
199    /// Sets the connection timeout.
200    pub fn with_connect_timeout_ms(mut self, timeout: u64) -> Self {
201        self.connect_timeout_ms = timeout;
202        self
203    }
204}
205
206/// A database backend that can execute storage operations.
207///
208/// This trait is inspired by Diesel's `Backend` trait, providing a common
209/// abstraction over different database drivers. Each backend implementation
210/// provides its own connection type and query builder.
211///
212/// # Design
213///
214/// The `Backend` trait is designed to be object-safe where possible, allowing
215/// for dynamic dispatch in composite storage scenarios. However, some operations
216/// require associated types for type safety.
217///
218/// # Example
219///
220/// ```ignore
221/// use helios_persistence::core::{Backend, BackendKind, BackendCapability};
222///
223/// // Check backend capabilities at runtime
224/// if backend.supports(BackendCapability::ChainedSearch) {
225///     // Use chained search
226/// } else {
227///     // Fall back to multiple queries
228/// }
229/// ```
230#[async_trait]
231pub trait Backend: Send + Sync + Debug {
232    /// The type of raw connection used by this backend.
233    type Connection: Send;
234
235    /// Returns the kind of backend.
236    fn kind(&self) -> BackendKind;
237
238    /// Returns a human-readable name for this backend.
239    fn name(&self) -> &'static str;
240
241    /// Checks if this backend supports the given capability.
242    fn supports(&self, capability: BackendCapability) -> bool;
243
244    /// Returns all capabilities supported by this backend.
245    fn capabilities(&self) -> Vec<BackendCapability>;
246
247    /// Acquires a connection from the pool.
248    async fn acquire(&self) -> Result<Self::Connection, BackendError>;
249
250    /// Returns the connection back to the pool.
251    async fn release(&self, conn: Self::Connection);
252
253    /// Checks if the backend is healthy and accepting connections.
254    async fn health_check(&self) -> Result<(), BackendError>;
255
256    /// Initializes the database schema if needed.
257    async fn initialize(&self) -> Result<(), BackendError>;
258
259    /// Runs any pending migrations.
260    async fn migrate(&self) -> Result<(), BackendError>;
261}
262
263/// Extension trait for backends that support connection pooling statistics.
264pub trait BackendPoolStats {
265    /// Returns the current number of active connections.
266    fn active_connections(&self) -> u32;
267
268    /// Returns the current number of idle connections.
269    fn idle_connections(&self) -> u32;
270
271    /// Returns the maximum pool size.
272    fn max_connections(&self) -> u32;
273
274    /// Returns the number of connections waiting to be acquired.
275    fn pending_connections(&self) -> u32;
276}
277
278/// Marker trait for backends that support ACID transactions.
279pub trait TransactionalBackend: Backend {}
280
281/// Marker trait for backends that support full-text search.
282pub trait FullTextBackend: Backend {}
283
284/// Marker trait for backends optimized for graph queries.
285pub trait GraphBackend: Backend {}
286
287/// Marker trait for backends optimized for time-series data.
288pub trait TimeSeriesBackend: Backend {}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_backend_kind_display() {
296        assert_eq!(BackendKind::Sqlite.to_string(), "sqlite");
297        assert_eq!(BackendKind::Postgres.to_string(), "postgres");
298        assert_eq!(BackendKind::Custom("custom-db").to_string(), "custom-db");
299    }
300
301    #[test]
302    fn test_backend_capability_display() {
303        assert_eq!(BackendCapability::Crud.to_string(), "crud");
304        assert_eq!(
305            BackendCapability::ChainedSearch.to_string(),
306            "chained-search"
307        );
308        assert_eq!(
309            BackendCapability::FullTextSearch.to_string(),
310            "full-text-search"
311        );
312        assert_eq!(
313            BackendCapability::BulkSubmitIngest.to_string(),
314            "bulk-submit-ingest"
315        );
316        assert_eq!(
317            BackendCapability::BulkSubmitRestWorker.to_string(),
318            "bulk-submit-rest-worker"
319        );
320    }
321
322    #[test]
323    fn test_backend_config_default() {
324        let config = BackendConfig::default();
325        assert_eq!(config.max_connections, 10);
326        assert_eq!(config.min_connections, 1);
327        assert_eq!(config.connect_timeout_ms, 5000);
328    }
329
330    #[test]
331    fn test_backend_config_builder() {
332        let config = BackendConfig::new("postgres://localhost/db")
333            .with_max_connections(20)
334            .with_connect_timeout_ms(10000);
335
336        assert_eq!(config.connection_string, "postgres://localhost/db");
337        assert_eq!(config.max_connections, 20);
338        assert_eq!(config.connect_timeout_ms, 10000);
339    }
340}