Skip to main content

rullst_orm/
lib.rs

1#[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
2pub use sqlx::AnyPool as EloquentPool;
3
4#[cfg(feature = "strict-postgres")]
5pub use sqlx::PgPool as EloquentPool;
6
7#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
8pub use sqlx::MySqlPool as EloquentPool;
9
10#[cfg(all(feature = "strict-sqlite", not(feature = "strict-postgres"), not(feature = "strict-mysql")))]
11pub use sqlx::SqlitePool as EloquentPool;
12
13#[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
14use sqlx::any::install_default_drivers;
15
16use std::sync::OnceLock;
17use std::sync::atomic::{AtomicUsize, Ordering};
18
19// Re-export the procedural macro so users only need to import `rullst-orm`
20pub use rullst_orm_macros::*;
21pub use sqlx;
22pub use futures;
23pub use serde;
24pub use serde_json;
25
26#[cfg(feature = "redis")]
27pub use redis;
28pub mod schema;
29pub mod collection;
30pub mod types;
31pub mod database;
32
33pub use types::Json;
34pub use collection::EloquentCollection;
35pub use database::EloquentDatabase;
36
37// Re-export async_trait so the macro can use it implicitly
38pub use async_trait::async_trait;
39
40// Re-export sqlx and FromRow for database mapping
41pub use sqlx::FromRow;
42pub use schema::{JoinClause, SubqueryBuilder};
43
44/// The global connection pool
45static DB_POOL: OnceLock<EloquentPool> = OnceLock::new();
46
47/// The driver identifier (postgres, mysql, sqlite) to help macro syntax formatting
48static DB_DRIVER: OnceLock<String> = OnceLock::new();
49
50/// The replica connection pools for read operations
51static REPLICA_POOLS: OnceLock<Vec<EloquentPool>> = OnceLock::new();
52
53/// Atomic index for replica round-robin selection
54static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
55
56#[cfg(feature = "redis")]
57static REDIS_CLIENT: OnceLock<redis::Client> = OnceLock::new();
58
59#[cfg(feature = "redis")]
60static REDIS_MANAGER: OnceLock<redis::aio::ConnectionManager> = OnceLock::new();
61
62/// Enum dinĂ¢mico para encapsular qualquer tipo que possa ser associado ao banco de dados pelo Macro
63#[derive(Clone, Debug)]
64pub enum EloquentValue {
65    String(String),
66    Int(i32),
67    Float(f64),
68    Bool(bool),
69}
70
71impl From<&str> for EloquentValue {
72    fn from(s: &str) -> Self { EloquentValue::String(s.to_string()) }
73}
74impl From<String> for EloquentValue {
75    fn from(s: String) -> Self { EloquentValue::String(s) }
76}
77impl From<i32> for EloquentValue {
78    fn from(i: i32) -> Self { EloquentValue::Int(i) }
79}
80impl From<f64> for EloquentValue {
81    fn from(f: f64) -> Self { EloquentValue::Float(f) }
82}
83impl From<bool> for EloquentValue {
84    fn from(b: bool) -> Self { EloquentValue::Bool(b) }
85}
86
87/// Eloquent configuration structure
88pub struct Eloquent;
89
90impl Eloquent {
91    /// Initialize the global database connection pool using an agnostic URI
92    pub async fn init(database_url: &str) -> Result<(), sqlx::Error> {
93        #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
94        install_default_drivers();
95
96        let pool = EloquentPool::connect(database_url).await?;
97        
98        if DB_POOL.set(pool).is_err() {
99            panic!("Eloquent has already been initialized");
100        }
101
102        let driver = if database_url.starts_with("postgres") {
103            "postgres"
104        } else if database_url.starts_with("mysql") {
105            "mysql"
106        } else {
107            "sqlite"
108        };
109        
110        let _ = DB_DRIVER.set(driver.to_string());
111        let _ = REPLICA_POOLS.set(vec![]);
112        
113        Ok(())
114    }
115
116    /// Initialize the global database connection pool and its read replicas
117    pub async fn init_with_replicas(primary_url: &str, replica_urls: Vec<&str>) -> Result<(), sqlx::Error> {
118        #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
119        install_default_drivers();
120
121        let pool = EloquentPool::connect(primary_url).await?;
122        
123        if DB_POOL.set(pool).is_err() {
124            panic!("Eloquent has already been initialized");
125        }
126
127        let driver = if primary_url.starts_with("postgres") {
128            "postgres"
129        } else if primary_url.starts_with("mysql") {
130            "mysql"
131        } else {
132            "sqlite"
133        };
134        
135        let _ = DB_DRIVER.set(driver.to_string());
136
137        let mut replicas = vec![];
138        for url in replica_urls {
139            let p = EloquentPool::connect(url).await?;
140            replicas.push(p);
141        }
142        let _ = REPLICA_POOLS.set(replicas);
143        
144        Ok(())
145    }
146
147    /// Retrieve the global database connection pool (strictly for writes)
148    pub fn pool() -> &'static EloquentPool {
149        DB_POOL.get().expect("Eloquent must be initialized before querying")
150    }
151
152    /// Retrieve the connection pool for read operations.
153    /// Performs a round-robin load balancing over replicas if configured.
154    pub fn read_pool() -> &'static EloquentPool {
155        if let Some(replicas) = REPLICA_POOLS.get()
156            && !replicas.is_empty() {
157                let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
158                return &replicas[idx];
159            }
160        Self::pool()
161    }
162
163    /// Retrieve the active driver string
164    pub fn driver() -> &'static str {
165        DB_DRIVER.get().expect("Eloquent must be initialized before querying").as_str()
166    }
167
168    /// Starts a new database transaction
169    #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
170    pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Any>, sqlx::Error> {
171        let pool = Self::pool();
172        pool.begin().await
173    }
174
175    #[cfg(feature = "strict-postgres")]
176    pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Postgres>, sqlx::Error> {
177        let pool = Self::pool();
178        pool.begin().await
179    }
180
181    #[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
182    pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::MySql>, sqlx::Error> {
183        let pool = Self::pool();
184        pool.begin().await
185    }
186
187    #[cfg(all(feature = "strict-sqlite", not(feature = "strict-postgres"), not(feature = "strict-mysql")))]
188    pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Sqlite>, sqlx::Error> {
189        let pool = Self::pool();
190        pool.begin().await
191    }
192
193    /// Run an array of seeders sequentially
194    pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), sqlx::Error> {
195        for seeder in seeders {
196            seeder.run().await?;
197        }
198        Ok(())
199    }
200
201    /// Enable query logging to print all queries to the terminal
202    pub fn enable_query_log() {
203        crate::schema::enable_query_log();
204    }
205
206    /// Disable query logging
207    pub fn disable_query_log() {
208        crate::schema::disable_query_log();
209    }
210
211    /// Initialize Redis connection and connection manager for caching and events
212    #[cfg(feature = "redis")]
213    pub async fn init_redis(redis_url: &str) -> Result<(), redis::RedisError> {
214        let client = redis::Client::open(redis_url)?;
215        let manager = redis::aio::ConnectionManager::new(client.clone()).await?;
216        let _ = REDIS_CLIENT.set(client);
217        let _ = REDIS_MANAGER.set(manager);
218        Ok(())
219    }
220
221    /// Get reference to the global Redis client
222    #[cfg(feature = "redis")]
223    pub fn redis_client() -> &'static redis::Client {
224        REDIS_CLIENT.get().expect("Redis must be initialized before using cache features")
225    }
226
227    /// Get clone of the thread-safe connection manager for async Redis queries
228    #[cfg(feature = "redis")]
229    pub fn redis_manager() -> redis::aio::ConnectionManager {
230        REDIS_MANAGER.get().expect("Redis must be initialized before using cache features").clone()
231    }
232}
233
234/// A database seeder trait for populating tables
235#[async_trait]
236pub trait Seeder: Send + Sync {
237    async fn run(&self) -> Result<(), sqlx::Error>;
238}
239
240/// The core trait that all Eloquent models will implement via #[derive(Eloquent)]
241#[async_trait]
242pub trait EloquentModel {
243    fn table_name() -> &'static str;
244}
245
246/// Represents a paginated result set
247#[derive(Debug, Clone)]
248pub struct PaginationResult<T> {
249    pub data: Vec<T>,
250    pub total: i64,
251    pub per_page: usize,
252    pub current_page: usize,
253    pub last_page: usize,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_eloquent_value_conversions() {
262        let v: EloquentValue = "test".into();
263        assert!(matches!(v, EloquentValue::String(_)));
264        let v_int: EloquentValue = 100.into();
265        assert!(matches!(v_int, EloquentValue::Int(100)));
266        let v_bool: EloquentValue = false.into();
267        assert!(matches!(v_bool, EloquentValue::Bool(false)));
268    }
269}