Skip to main content

rullst_orm/
lib.rs

1#[cfg(not(any(
2    feature = "strict-postgres",
3    feature = "strict-mysql",
4    feature = "strict-sqlite"
5)))]
6pub use sqlx::AnyPool as RullstPool;
7
8#[cfg(feature = "strict-postgres")]
9pub use sqlx::PgPool as RullstPool;
10
11#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
12pub use sqlx::MySqlPool as RullstPool;
13
14#[cfg(all(
15    feature = "strict-sqlite",
16    not(feature = "strict-postgres"),
17    not(feature = "strict-mysql")
18))]
19pub use sqlx::SqlitePool as RullstPool;
20
21#[cfg(not(any(
22    feature = "strict-postgres",
23    feature = "strict-mysql",
24    feature = "strict-sqlite"
25)))]
26use sqlx::any::install_default_drivers;
27
28use std::sync::OnceLock;
29use std::sync::atomic::{AtomicUsize, Ordering};
30
31// Hide underlying libraries for macro usage while keeping the public API clean
32#[doc(hidden)]
33pub use futures as _futures;
34#[doc(hidden)]
35pub use serde as _serde;
36#[doc(hidden)]
37pub use serde_json as _serde_json;
38#[doc(hidden)]
39pub use sqlx as _sqlx;
40
41#[cfg(feature = "redis")]
42#[doc(hidden)]
43pub use redis as _redis;
44pub mod admin;
45pub mod audit;
46pub mod collection;
47pub mod database;
48pub mod db;
49pub mod error;
50pub mod resource;
51pub mod schema;
52pub mod scout;
53pub mod tenant;
54pub mod types;
55
56// Export the custom Error enum to the root
57pub use error::RullstError as Error;
58
59// Re-exports
60pub use _sqlx::FromRow;
61pub use admin::dashboard_html;
62pub use collection::RullstCollection;
63pub use database::RullstDatabase;
64pub use resource::{ApiResource, JsonResource, ResourceCollection};
65pub use rullst_orm_macros::Orm;
66pub use scout::{SearchEngine, get_search_engine, set_search_engine};
67pub use tenant::{get_tenant_id, with_tenant};
68pub use types::Json;
69
70// Re-export async_trait so the macro can use it implicitly
71pub use async_trait::async_trait;
72
73// Re-export sqlx and FromRow for database mapping
74pub use schema::{JoinClause, SubqueryBuilder};
75
76/// The global connection pool
77static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
78
79/// The driver identifier (postgres, mysql, sqlite) to help macro syntax formatting
80static DB_DRIVER: OnceLock<String> = OnceLock::new();
81
82/// The replica connection pools for read operations
83static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
84
85/// Atomic index for replica round-robin selection
86static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
87
88#[cfg(feature = "redis")]
89static REDIS_CLIENT: OnceLock<_redis::Client> = OnceLock::new();
90
91#[cfg(feature = "redis")]
92static REDIS_MANAGER: OnceLock<_redis::aio::ConnectionManager> = OnceLock::new();
93
94/// Enum dinâmico para encapsular qualquer tipo que possa ser associado ao banco de dados pelo Macro
95#[derive(Clone, Debug)]
96pub enum RullstValue {
97    String(String),
98    Int(i32),
99    Float(f64),
100    Bool(bool),
101}
102
103impl From<&str> for RullstValue {
104    fn from(s: &str) -> Self {
105        RullstValue::String(s.to_string())
106    }
107}
108impl From<String> for RullstValue {
109    fn from(s: String) -> Self {
110        RullstValue::String(s)
111    }
112}
113impl From<i32> for RullstValue {
114    fn from(i: i32) -> Self {
115        RullstValue::Int(i)
116    }
117}
118impl From<f64> for RullstValue {
119    fn from(f: f64) -> Self {
120        RullstValue::Float(f)
121    }
122}
123impl From<bool> for RullstValue {
124    fn from(b: bool) -> Self {
125        RullstValue::Bool(b)
126    }
127}
128
129impl TryFrom<RullstValue> for String {
130    type Error = &'static str;
131    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
132        match val {
133            RullstValue::String(s) => Ok(s),
134            _ => Err("Not a string"),
135        }
136    }
137}
138impl TryFrom<RullstValue> for i32 {
139    type Error = &'static str;
140    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
141        match val {
142            RullstValue::Int(i) => Ok(i),
143            _ => Err("Not an i32"),
144        }
145    }
146}
147impl TryFrom<RullstValue> for f64 {
148    type Error = &'static str;
149    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
150        match val {
151            RullstValue::Float(f) => Ok(f),
152            _ => Err("Not an f64"),
153        }
154    }
155}
156impl TryFrom<RullstValue> for bool {
157    type Error = &'static str;
158    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
159        match val {
160            RullstValue::Bool(b) => Ok(b),
161            _ => Err("Not a bool"),
162        }
163    }
164}
165
166/// Orm configuration structure
167pub struct Orm;
168
169impl Orm {
170    /// Initialize the global database connection pool using an agnostic URI
171    pub async fn init(database_url: &str) -> Result<(), crate::Error> {
172        #[cfg(not(any(
173            feature = "strict-postgres",
174            feature = "strict-mysql",
175            feature = "strict-sqlite"
176        )))]
177        install_default_drivers();
178
179        let pool = RullstPool::connect(database_url).await?;
180
181        if DB_POOL.set(pool).is_err() {
182            return Err(crate::Error::Internal(
183                "Orm has already been initialized".to_string(),
184            ));
185        }
186
187        let driver = if database_url.starts_with("postgres") {
188            "postgres"
189        } else if database_url.starts_with("mysql") {
190            "mysql"
191        } else {
192            "sqlite"
193        };
194
195        let _ = DB_DRIVER.set(driver.to_string());
196        let _ = REPLICA_POOLS.set(vec![]);
197
198        Ok(())
199    }
200
201    /// Initialize the global database connection pool and its read replicas
202    pub async fn init_with_replicas(
203        primary_url: &str,
204        replica_urls: Vec<&str>,
205    ) -> Result<(), crate::Error> {
206        #[cfg(not(any(
207            feature = "strict-postgres",
208            feature = "strict-mysql",
209            feature = "strict-sqlite"
210        )))]
211        install_default_drivers();
212
213        let pool = RullstPool::connect(primary_url).await?;
214
215        if DB_POOL.set(pool).is_err() {
216            return Err(crate::Error::Internal(
217                "Orm has already been initialized".to_string(),
218            ));
219        }
220
221        let driver = if primary_url.starts_with("postgres") {
222            "postgres"
223        } else if primary_url.starts_with("mysql") {
224            "mysql"
225        } else {
226            "sqlite"
227        };
228
229        let _ = DB_DRIVER.set(driver.to_string());
230
231        // Initialize all replica pools concurrently — each connect() is independent I/O.
232        let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
233        let replicas = futures::future::try_join_all(replica_futures).await?;
234        let _ = REPLICA_POOLS.set(replicas);
235
236        Ok(())
237    }
238
239    /// Retrieve the global database connection pool (strictly for writes)
240    pub fn pool() -> &'static RullstPool {
241        DB_POOL
242            .get()
243            .expect("Orm must be initialized before querying")
244    }
245
246    /// Retrieve the connection pool for read operations.
247    /// Performs a round-robin load balancing over replicas if configured.
248    pub fn read_pool() -> &'static RullstPool {
249        if let Some(replicas) = REPLICA_POOLS.get()
250            && !replicas.is_empty()
251        {
252            let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
253            return &replicas[idx];
254        }
255        Self::pool()
256    }
257
258    /// Retrieve the active driver string
259    pub fn driver() -> &'static str {
260        DB_DRIVER
261            .get()
262            .expect("Orm must be initialized before querying")
263            .as_str()
264    }
265
266    pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
267        let pool = Self::pool();
268        pool.begin().await.map_err(Into::into)
269    }
270
271    /// Run an array of seeders sequentially
272    pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
273        for seeder in seeders {
274            seeder.run().await?;
275        }
276        Ok(())
277    }
278
279    /// Enable query logging to print all queries to the terminal
280    pub fn enable_query_log() {
281        crate::schema::enable_query_log();
282    }
283
284    /// Disable query logging
285    pub fn disable_query_log() {
286        crate::schema::disable_query_log();
287    }
288
289    /// Initialize Redis connection and connection manager for caching and events
290    #[cfg(feature = "redis")]
291    pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
292        let client = _redis::Client::open(redis_url)?;
293        let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
294        let _ = REDIS_CLIENT.set(client);
295        let _ = REDIS_MANAGER.set(manager);
296        Ok(())
297    }
298
299    /// Get reference to the global Redis client
300    #[cfg(feature = "redis")]
301    pub fn redis_client() -> Result<&'static _redis::Client, crate::Error> {
302        REDIS_CLIENT.get().ok_or_else(|| {
303            crate::Error::Internal(
304                "Orm::init_redis() must be called before using cache features".to_string(),
305            )
306        })
307    }
308
309    /// Get clone of the thread-safe connection manager for async Redis queries
310    #[cfg(feature = "redis")]
311    pub fn redis_manager() -> Result<_redis::aio::ConnectionManager, crate::Error> {
312        REDIS_MANAGER.get().cloned().ok_or_else(|| {
313            crate::Error::Internal(
314                "Orm::init_redis() must be called before using cache features".to_string(),
315            )
316        })
317    }
318}
319
320/// A database seeder trait for populating tables
321#[async_trait]
322pub trait Seeder: Send + Sync {
323    async fn run(&self) -> Result<(), crate::Error>;
324}
325
326/// The core trait that all Orm models will implement via #[derive(Orm)]
327#[async_trait]
328pub trait RullstModel {
329    fn table_name() -> &'static str;
330}
331
332/// Represents a paginated result set
333#[derive(Debug, Clone)]
334pub struct PaginationResult<T> {
335    pub data: Vec<T>,
336    pub total: i64,
337    pub per_page: usize,
338    pub current_page: usize,
339    pub last_page: usize,
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_pagination_result() {
348        let mut pr = PaginationResult {
349            data: vec![1, 2, 3],
350            total: 3,
351            per_page: 10,
352            current_page: 1,
353            last_page: 1,
354        };
355        assert_eq!(pr.data.len(), 3);
356        assert_eq!(pr.total, 3);
357        pr.data.push(4);
358        assert_eq!(pr.data.len(), 4);
359    }
360
361    #[test]
362    fn test_rullst_value_conversions() {
363        let v: RullstValue = "test".into();
364        assert!(matches!(v, RullstValue::String(_)));
365        let v_int: RullstValue = 100.into();
366        assert!(matches!(v_int, RullstValue::Int(100)));
367        let v_bool: RullstValue = false.into();
368        assert!(matches!(v_bool, RullstValue::Bool(false)));
369    }
370
371    #[test]
372    fn test_enable_query_log_wrapper() {
373        // Orm::enable/disable_query_log delegate to schema — verify the delegation works.
374        Orm::disable_query_log();
375        assert!(!crate::schema::is_query_log_enabled());
376        Orm::enable_query_log();
377        assert!(crate::schema::is_query_log_enabled());
378        Orm::disable_query_log();
379        assert!(!crate::schema::is_query_log_enabled());
380    }
381
382    #[test]
383    fn test_disable_query_log_wrapper() {
384        Orm::enable_query_log();
385        Orm::disable_query_log();
386        assert!(!crate::schema::is_query_log_enabled());
387    }
388
389    #[cfg(feature = "redis")]
390    #[test]
391    fn test_redis_client_uninitialized() {
392        let err = Orm::redis_client().unwrap_err();
393        assert!(matches!(err, crate::Error::Internal(_)));
394    }
395
396    #[cfg(feature = "redis")]
397    #[test]
398    fn test_redis_manager_uninitialized() {
399        let err = Orm::redis_manager().unwrap_err();
400        assert!(matches!(err, crate::Error::Internal(_)));
401    }
402}