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(not(any(
9    feature = "strict-postgres",
10    feature = "strict-mysql",
11    feature = "strict-sqlite"
12)))]
13pub use sqlx::any::AnyPoolOptions as RullstPoolOptions;
14
15#[cfg(feature = "strict-postgres")]
16pub use sqlx::PgPool as RullstPool;
17
18#[cfg(feature = "strict-postgres")]
19pub use sqlx::postgres::PgPoolOptions as RullstPoolOptions;
20
21#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
22pub use sqlx::MySqlPool as RullstPool;
23
24#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
25pub use sqlx::mysql::MySqlPoolOptions as RullstPoolOptions;
26
27#[cfg(all(
28    feature = "strict-sqlite",
29    not(feature = "strict-postgres"),
30    not(feature = "strict-mysql")
31))]
32pub use sqlx::SqlitePool as RullstPool;
33
34#[cfg(all(
35    feature = "strict-sqlite",
36    not(feature = "strict-postgres"),
37    not(feature = "strict-mysql")
38))]
39pub use sqlx::sqlite::SqlitePoolOptions as RullstPoolOptions;
40
41#[cfg(not(any(
42    feature = "strict-postgres",
43    feature = "strict-mysql",
44    feature = "strict-sqlite"
45)))]
46use sqlx::any::install_default_drivers;
47
48use std::sync::OnceLock;
49use std::sync::atomic::{AtomicUsize, Ordering};
50
51// Hide underlying libraries for macro usage while keeping the public API clean
52#[doc(hidden)]
53pub use async_stream as _async_stream;
54#[doc(hidden)]
55pub use futures as _futures;
56#[doc(hidden)]
57pub use serde as _serde;
58#[doc(hidden)]
59pub use serde_json as _serde_json;
60#[doc(hidden)]
61pub use sqlx as _sqlx;
62#[doc(hidden)]
63pub use tracing as _tracing;
64
65#[cfg(feature = "redis")]
66#[doc(hidden)]
67pub use redis as _redis;
68
69/// Helper to convert `?` placeholders to `$1`, `$2` etc. for Postgres.
70#[doc(hidden)]
71pub fn replace_placeholders(sql: &str) -> String {
72    let mut replaced = String::with_capacity(sql.len() + 10);
73    let mut last_idx = 0;
74    for (counter, (idx, _)) in (1..).zip(sql.match_indices('?')) {
75        replaced.push_str(&sql[last_idx..idx]);
76        use std::fmt::Write;
77        write!(replaced, "${}", counter).unwrap();
78        last_idx = idx + 1;
79    }
80    replaced.push_str(&sql[last_idx..]);
81    replaced
82}
83
84static PREVENT_LAZY_LOADING: std::sync::atomic::AtomicBool =
85    std::sync::atomic::AtomicBool::new(false);
86
87/// Prevents relationships from being lazily loaded when accessed without being eager loaded.
88/// When enabled, attempting to lazily load a relation will throw a panic in development.
89pub fn prevent_lazy_loading(prevent: bool) {
90    PREVENT_LAZY_LOADING.store(prevent, std::sync::atomic::Ordering::Relaxed);
91}
92
93#[doc(hidden)]
94pub fn is_lazy_loading_prevented() -> bool {
95    PREVENT_LAZY_LOADING.load(std::sync::atomic::Ordering::Relaxed)
96}
97
98pub mod admin;
99pub mod audit;
100pub mod collection;
101pub mod database;
102pub mod db;
103pub mod policy;
104
105tokio::task_local! {
106    pub static CURRENT_TX: std::sync::Arc<tokio::sync::Mutex<Option<crate::db::Transaction<'static>>>>;
107}
108
109#[macro_export]
110macro_rules! execute_query {
111    ($query:expr, $method:ident, $pool_fn:ident) => {
112        if let Ok(tx_arc) = $crate::CURRENT_TX.try_with(|tx| tx.clone()) {
113            let mut tx_guard = tx_arc.lock().await;
114            if let Some(tx) = tx_guard.as_mut() {
115                $query.$method(&mut **tx).await
116            } else {
117                let pool = $crate::Orm::$pool_fn();
118                $query.$method(pool).await
119            }
120        } else {
121            let pool = $crate::Orm::$pool_fn();
122            $query.$method(pool).await
123        }
124    };
125}
126
127#[macro_export]
128macro_rules! dispatch_executor {
129    ($pool_fn:ident, |$executor:ident| $e:expr) => {
130        if let Ok(tx_arc) = $crate::CURRENT_TX.try_with(|tx| tx.clone()) {
131            let mut tx_guard = tx_arc.lock().await;
132            if let Some(tx) = tx_guard.as_mut() {
133                let $executor = &mut **tx;
134                $e
135            } else {
136                let $executor = $crate::Orm::$pool_fn();
137                $e
138            }
139        } else {
140            let $executor = $crate::Orm::$pool_fn();
141            $e
142        }
143    };
144}
145
146pub mod error;
147pub mod privacy;
148pub mod raw;
149pub mod resource;
150pub mod schema;
151pub mod scout;
152pub mod tenant;
153pub mod types;
154
155// Export the custom Error enum to the root
156pub use error::RullstError as Error;
157
158// Re-exports
159pub use _sqlx::FromRow;
160pub use admin::dashboard_html;
161pub use collection::RullstCollection;
162pub use database::RullstDatabase;
163pub use policy::Policy;
164pub use privacy::{ComplianceModel, PrivacyReport, SecretString};
165pub use resource::{ApiResource, JsonResource, ResourceCollection};
166pub use rullst_orm_macros::{Enum, Orm, PersonalData, test};
167pub use scout::{SearchEngine, get_search_engine, set_search_engine};
168pub use tenant::{get_tenant_id, with_tenant};
169pub use types::Json;
170
171// Re-export async_trait so the macro can use it implicitly
172pub use async_trait::async_trait;
173
174// Re-export sqlx and FromRow for database mapping
175pub use schema::{JoinClause, SubqueryBuilder};
176
177/// The global connection pool
178static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
179
180/// The driver identifier (postgres, mysql, sqlite) to help macro syntax formatting
181static DB_DRIVER: OnceLock<String> = OnceLock::new();
182
183/// The replica connection pools for read operations
184static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
185
186/// Atomic index for replica round-robin selection
187static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
188
189#[cfg(feature = "redis")]
190static REDIS_CLIENT: OnceLock<_redis::Client> = OnceLock::new();
191
192#[cfg(feature = "redis")]
193static REDIS_MANAGER: OnceLock<_redis::aio::ConnectionManager> = OnceLock::new();
194
195/// Enum dinâmico para encapsular qualquer tipo que possa ser associado ao banco de dados pelo Macro
196#[derive(Clone, Debug)]
197pub enum RullstValue {
198    String(String),
199    Int(i32),
200    Float(f64),
201    Bool(bool),
202}
203
204impl From<&str> for RullstValue {
205    fn from(s: &str) -> Self {
206        RullstValue::String(s.to_string())
207    }
208}
209impl From<String> for RullstValue {
210    fn from(s: String) -> Self {
211        RullstValue::String(s)
212    }
213}
214impl From<i32> for RullstValue {
215    fn from(i: i32) -> Self {
216        RullstValue::Int(i)
217    }
218}
219impl From<f64> for RullstValue {
220    fn from(f: f64) -> Self {
221        RullstValue::Float(f)
222    }
223}
224impl From<bool> for RullstValue {
225    fn from(b: bool) -> Self {
226        RullstValue::Bool(b)
227    }
228}
229
230impl TryFrom<RullstValue> for String {
231    type Error = &'static str;
232    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
233        match val {
234            RullstValue::String(s) => Ok(s),
235            _ => Err("Not a string"),
236        }
237    }
238}
239impl TryFrom<RullstValue> for i32 {
240    type Error = &'static str;
241    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
242        match val {
243            RullstValue::Int(i) => Ok(i),
244            _ => Err("Not an i32"),
245        }
246    }
247}
248impl TryFrom<RullstValue> for f64 {
249    type Error = &'static str;
250    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
251        match val {
252            RullstValue::Float(f) => Ok(f),
253            _ => Err("Not an f64"),
254        }
255    }
256}
257impl TryFrom<RullstValue> for bool {
258    type Error = &'static str;
259    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
260        match val {
261            RullstValue::Bool(b) => Ok(b),
262            _ => Err("Not a bool"),
263        }
264    }
265}
266
267/// Orm configuration structure
268pub struct Orm;
269
270impl Orm {
271    /// Initialize the global database connection pool using an agnostic URI
272    pub async fn init(database_url: &str) -> Result<(), crate::Error> {
273        Self::validate_dsn(database_url);
274
275        #[cfg(not(any(
276            feature = "strict-postgres",
277            feature = "strict-mysql",
278            feature = "strict-sqlite"
279        )))]
280        install_default_drivers();
281
282        let pool = RullstPool::connect(database_url).await?;
283
284        if DB_POOL.set(pool).is_err() {
285            return Err(crate::Error::Internal(
286                "Orm has already been initialized".to_string(),
287            ));
288        }
289
290        let driver = if database_url.starts_with("postgres") {
291            "postgres"
292        } else if database_url.starts_with("mysql") {
293            "mysql"
294        } else {
295            "sqlite"
296        };
297
298        let _ = DB_DRIVER.set(driver.to_string());
299        let _ = REPLICA_POOLS.set(vec![]);
300
301        Ok(())
302    }
303
304    /// Initialize the global database connection pool with specific pool options
305    pub async fn init_with_options(
306        database_url: &str,
307        max_connections: u32,
308        acquire_timeout_secs: u64,
309    ) -> Result<(), crate::Error> {
310        Self::validate_dsn(database_url);
311
312        #[cfg(not(any(
313            feature = "strict-postgres",
314            feature = "strict-mysql",
315            feature = "strict-sqlite"
316        )))]
317        install_default_drivers();
318
319        let pool = RullstPoolOptions::new()
320            .max_connections(max_connections)
321            .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs))
322            .connect(database_url)
323            .await?;
324
325        if DB_POOL.set(pool).is_err() {
326            return Err(crate::Error::Internal(
327                "Orm has already been initialized".to_string(),
328            ));
329        }
330
331        let driver = if database_url.starts_with("postgres") {
332            "postgres"
333        } else if database_url.starts_with("mysql") {
334            "mysql"
335        } else {
336            "sqlite"
337        };
338
339        let _ = DB_DRIVER.set(driver.to_string());
340        let _ = REPLICA_POOLS.set(vec![]);
341
342        Ok(())
343    }
344
345    #[cfg_attr(test, mutants::skip)]
346    fn validate_dsn(database_url: &str) {
347        if database_url.contains("sslmode=disable")
348            && !database_url.contains("localhost")
349            && !database_url.contains("127.0.0.1")
350        {
351            eprintln!(
352                "⚠️ [SECURITY WARNING] Rullst ORM: TLS/SSL disabled on external database connection! This is highly discouraged in production environments."
353            );
354        }
355    }
356
357    /// Initialize the global database connection pool and its read replicas
358    pub async fn init_with_replicas(
359        primary_url: &str,
360        replica_urls: Vec<&str>,
361    ) -> Result<(), crate::Error> {
362        #[cfg(not(any(
363            feature = "strict-postgres",
364            feature = "strict-mysql",
365            feature = "strict-sqlite"
366        )))]
367        install_default_drivers();
368
369        let pool = RullstPool::connect(primary_url).await?;
370
371        if DB_POOL.set(pool).is_err() {
372            return Err(crate::Error::Internal(
373                "Orm has already been initialized".to_string(),
374            ));
375        }
376
377        let driver = if primary_url.starts_with("postgres") {
378            "postgres"
379        } else if primary_url.starts_with("mysql") {
380            "mysql"
381        } else {
382            "sqlite"
383        };
384
385        let _ = DB_DRIVER.set(driver.to_string());
386
387        // Initialize all replica pools concurrently — each connect() is independent I/O.
388        let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
389        let replicas = futures::future::try_join_all(replica_futures).await?;
390        let _ = REPLICA_POOLS.set(replicas);
391
392        Ok(())
393    }
394
395    /// Retrieve the global database connection pool (strictly for writes)
396    pub fn pool() -> &'static RullstPool {
397        DB_POOL
398            .get()
399            .expect("Orm must be initialized before querying")
400    }
401
402    /// Retrieve the connection pool for read operations.
403    /// Performs a round-robin load balancing over replicas if configured.
404    #[cfg_attr(test, mutants::skip)]
405    pub fn read_pool() -> &'static RullstPool {
406        if let Some(replicas) = REPLICA_POOLS.get()
407            && !replicas.is_empty()
408        {
409            let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
410            return &replicas[idx];
411        }
412        Self::pool()
413    }
414
415    /// Retrieve the active driver string
416    pub fn driver() -> &'static str {
417        DB_DRIVER
418            .get()
419            .expect("Orm must be initialized before querying")
420            .as_str()
421    }
422
423    /// Create a raw SQL query builder.
424    pub fn raw(sql: &str) -> raw::RawQueryBuilder {
425        raw::RawQueryBuilder::new(sql)
426    }
427
428    pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
429        let pool = Self::pool();
430        pool.begin().await.map_err(Into::into)
431    }
432
433    /// Run an array of seeders sequentially
434    #[cfg_attr(test, mutants::skip)]
435    pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
436        for seeder in seeders {
437            seeder.run().await?;
438        }
439        Ok(())
440    }
441
442    /// Enable query logging to print all queries to the terminal
443    pub fn enable_query_log() {
444        crate::schema::enable_query_log();
445    }
446
447    /// Disable query logging
448    pub fn disable_query_log() {
449        crate::schema::disable_query_log();
450    }
451
452    /// Set a global maximum limit for all queries without an explicit limit override
453    pub fn set_max_query_limit(limit: usize) {
454        crate::schema::set_max_query_limit(limit);
455    }
456
457    /// Set a global maximum execution timeout for all queries
458    pub fn set_query_timeout(secs: u64) {
459        crate::schema::set_query_timeout(secs);
460    }
461
462    /// Initialize Redis connection and connection manager for caching and events
463    #[cfg(feature = "redis")]
464    #[cfg_attr(test, mutants::skip)]
465    pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
466        let client = _redis::Client::open(redis_url)?;
467        let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
468        let _ = REDIS_CLIENT.set(client);
469        let _ = REDIS_MANAGER.set(manager);
470        Ok(())
471    }
472
473    /// Get reference to the global Redis client
474    #[cfg(feature = "redis")]
475    #[cfg_attr(test, mutants::skip)]
476    pub fn redis_client() -> Result<&'static _redis::Client, crate::Error> {
477        REDIS_CLIENT.get().ok_or_else(|| {
478            crate::Error::Internal(
479                "Orm::init_redis() must be called before using cache features".to_string(),
480            )
481        })
482    }
483
484    /// Get clone of the thread-safe connection manager for async Redis queries
485    #[cfg(feature = "redis")]
486    #[cfg_attr(test, mutants::skip)]
487    pub fn redis_manager() -> Result<_redis::aio::ConnectionManager, crate::Error> {
488        REDIS_MANAGER.get().cloned().ok_or_else(|| {
489            crate::Error::Internal(
490                "Orm::init_redis() must be called before using cache features".to_string(),
491            )
492        })
493    }
494}
495
496/// A database seeder trait for populating tables
497#[async_trait]
498pub trait Seeder: Send + Sync {
499    async fn run(&self) -> Result<(), crate::Error>;
500}
501
502/// The core trait that all Orm models will implement via #[derive(Orm)]
503#[async_trait]
504pub trait RullstModel {
505    fn table_name() -> &'static str;
506}
507
508/// Represents a paginated result set
509#[derive(Debug, Clone)]
510pub struct PaginationResult<T> {
511    pub data: Vec<T>,
512    pub total: i64,
513    pub per_page: usize,
514    pub current_page: usize,
515    pub last_page: usize,
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use ::core::prelude::v1::test;
522
523    #[test]
524    fn test_pagination_result() {
525        let mut pr = PaginationResult {
526            data: vec![1, 2, 3],
527            total: 3,
528            per_page: 10,
529            current_page: 1,
530            last_page: 1,
531        };
532        assert_eq!(pr.data.len(), 3);
533        assert_eq!(pr.total, 3);
534        pr.data.push(4);
535        assert_eq!(pr.data.len(), 4);
536    }
537
538    #[test]
539    fn test_replace_placeholders() {
540        assert_eq!(
541            super::replace_placeholders("SELECT * FROM users WHERE id = ? AND name = ?"),
542            "SELECT * FROM users WHERE id = $1 AND name = $2"
543        );
544        assert_eq!(
545            super::replace_placeholders("INSERT INTO users (name) VALUES (?)"),
546            "INSERT INTO users (name) VALUES ($1)"
547        );
548        assert_eq!(
549            super::replace_placeholders("SELECT * FROM users"),
550            "SELECT * FROM users"
551        );
552        assert_eq!(super::replace_placeholders("? ? ?"), "$1 $2 $3");
553    }
554
555    #[test]
556    fn test_rullst_value_conversions() {
557        // From
558        let v: RullstValue = "test".into();
559        assert!(matches!(v, RullstValue::String(_)));
560        let v_string: RullstValue = "test".to_string().into();
561        assert!(matches!(v_string, RullstValue::String(_)));
562        let v_int: RullstValue = 100.into();
563        assert!(matches!(v_int, RullstValue::Int(100)));
564        let v_bool: RullstValue = false.into();
565        assert!(matches!(v_bool, RullstValue::Bool(false)));
566        let v_float: RullstValue = std::f64::consts::PI.into();
567        assert!(matches!(v_float, RullstValue::Float(_)));
568
569        // TryFrom String
570        let v_str_conv = RullstValue::String("hello".to_string());
571        assert_eq!(String::try_from(v_str_conv).unwrap(), "hello");
572        assert!(String::try_from(RullstValue::Int(10)).is_err());
573
574        // TryFrom i32
575        let v_int_conv = RullstValue::Int(42);
576        assert_eq!(i32::try_from(v_int_conv).unwrap(), 42);
577        assert!(i32::try_from(RullstValue::Bool(true)).is_err());
578
579        // TryFrom f64
580        let v_float_conv = RullstValue::Float(2.71);
581        assert_eq!(f64::try_from(v_float_conv).unwrap(), 2.71);
582        assert!(f64::try_from(RullstValue::Int(10)).is_err());
583
584        // TryFrom bool
585        let v_bool_conv = RullstValue::Bool(true);
586        assert!(bool::try_from(v_bool_conv).unwrap());
587        assert!(bool::try_from(RullstValue::Int(10)).is_err());
588    }
589
590    #[test]
591    fn test_enable_query_log_wrapper() {
592        // Orm::enable/disable_query_log delegate to schema — verify the delegation works.
593        Orm::disable_query_log();
594        assert!(!crate::schema::is_query_log_enabled());
595        Orm::enable_query_log();
596        assert!(crate::schema::is_query_log_enabled());
597        Orm::disable_query_log();
598        assert!(!crate::schema::is_query_log_enabled());
599    }
600
601    #[test]
602    fn test_disable_query_log_wrapper() {
603        Orm::enable_query_log();
604        Orm::disable_query_log();
605        assert!(!crate::schema::is_query_log_enabled());
606    }
607
608    #[cfg(feature = "redis")]
609    #[test]
610    fn test_redis_client_uninitialized() {
611        let err = Orm::redis_client().unwrap_err();
612        assert!(matches!(err, crate::Error::Internal(_)));
613    }
614
615    #[cfg(feature = "redis")]
616    #[test]
617    fn test_redis_manager_uninitialized() {
618        let err = Orm::redis_manager().unwrap_err();
619        assert!(matches!(err, crate::Error::Internal(_)));
620    }
621
622    #[test]
623    #[should_panic(expected = "Orm must be initialized before querying")]
624    fn test_pool_uninitialized() {
625        let _ = Orm::pool();
626    }
627
628    #[test]
629    #[should_panic(expected = "Orm must be initialized before querying")]
630    fn test_driver_uninitialized() {
631        let _ = Orm::driver();
632    }
633
634    #[test]
635    #[should_panic(expected = "Orm must be initialized before querying")]
636    fn test_read_pool_uninitialized() {
637        let _ = Orm::read_pool();
638    }
639
640    #[test]
641    fn test_validate_dsn() {
642        // Safe case
643        Orm::validate_dsn("sqlite::memory:");
644        // Security warning case (printed to stderr, shouldn't panic)
645        Orm::validate_dsn("postgres://external-db.com/mydb?sslmode=disable");
646    }
647
648    #[cfg(feature = "redis")]
649    #[tokio::test]
650    async fn test_init_redis_failure() {
651        let err = Orm::init_redis("redis://127.0.0.1:0").await.unwrap_err();
652        assert!(matches!(err, crate::Error::CacheError(_)));
653    }
654
655    #[test]
656    fn test_orm_max_query_limit_and_timeout() {
657        Orm::set_max_query_limit(15);
658        assert_eq!(crate::schema::get_max_query_limit(), Some(15));
659        Orm::set_max_query_limit(0);
660        assert_eq!(crate::schema::get_max_query_limit(), None);
661
662        Orm::set_query_timeout(5);
663        assert_eq!(
664            crate::schema::get_query_timeout(),
665            Some(std::time::Duration::from_secs(5))
666        );
667        Orm::set_query_timeout(0);
668        assert_eq!(crate::schema::get_query_timeout(), None);
669    }
670}