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#[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#[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
87pub 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
155pub use error::RullstError as Error;
157
158pub 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
171pub use async_trait::async_trait;
173
174pub use schema::{JoinClause, SubqueryBuilder};
176
177static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
179
180static DB_DRIVER: OnceLock<String> = OnceLock::new();
182
183static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
185
186static 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#[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
267pub struct Orm;
269
270impl Orm {
271 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 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 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 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 pub fn pool() -> &'static RullstPool {
397 DB_POOL
398 .get()
399 .expect("Orm must be initialized before querying")
400 }
401
402 #[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 pub fn driver() -> &'static str {
417 DB_DRIVER
418 .get()
419 .expect("Orm must be initialized before querying")
420 .as_str()
421 }
422
423 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 #[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 pub fn enable_query_log() {
444 crate::schema::enable_query_log();
445 }
446
447 pub fn disable_query_log() {
449 crate::schema::disable_query_log();
450 }
451
452 pub fn set_max_query_limit(limit: usize) {
454 crate::schema::set_max_query_limit(limit);
455 }
456
457 pub fn set_query_timeout(secs: u64) {
459 crate::schema::set_query_timeout(secs);
460 }
461
462 #[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 #[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 #[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#[async_trait]
498pub trait Seeder: Send + Sync {
499 async fn run(&self) -> Result<(), crate::Error>;
500}
501
502#[async_trait]
504pub trait RullstModel {
505 fn table_name() -> &'static str;
506}
507
508#[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 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 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 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 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 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::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 Orm::validate_dsn("sqlite::memory:");
644 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}