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 futures as _futures;
54#[doc(hidden)]
55pub use serde as _serde;
56#[doc(hidden)]
57pub use serde_json as _serde_json;
58#[doc(hidden)]
59pub use sqlx as _sqlx;
60
61#[cfg(feature = "redis")]
62#[doc(hidden)]
63pub use redis as _redis;
64
65#[doc(hidden)]
67pub fn replace_placeholders(sql: &str) -> String {
68 let mut replaced = String::with_capacity(sql.len() + 10);
69 let mut last_idx = 0;
70 for (counter, (idx, _)) in (1..).zip(sql.match_indices('?')) {
71 replaced.push_str(&sql[last_idx..idx]);
72 use std::fmt::Write;
73 write!(replaced, "${}", counter).unwrap();
74 last_idx = idx + 1;
75 }
76 replaced.push_str(&sql[last_idx..]);
77 replaced
78}
79
80pub mod admin;
81pub mod audit;
82pub mod collection;
83pub mod database;
84pub mod db;
85pub mod error;
86pub mod resource;
87pub mod schema;
88pub mod scout;
89pub mod tenant;
90pub mod types;
91
92pub use error::RullstError as Error;
94
95pub use _sqlx::FromRow;
97pub use admin::dashboard_html;
98pub use collection::RullstCollection;
99pub use database::RullstDatabase;
100pub use resource::{ApiResource, JsonResource, ResourceCollection};
101pub use rullst_orm_macros::Orm;
102pub use scout::{SearchEngine, get_search_engine, set_search_engine};
103pub use tenant::{get_tenant_id, with_tenant};
104pub use types::Json;
105
106pub use async_trait::async_trait;
108
109pub use schema::{JoinClause, SubqueryBuilder};
111
112static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
114
115static DB_DRIVER: OnceLock<String> = OnceLock::new();
117
118static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
120
121static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
123
124#[cfg(feature = "redis")]
125static REDIS_CLIENT: OnceLock<_redis::Client> = OnceLock::new();
126
127#[cfg(feature = "redis")]
128static REDIS_MANAGER: OnceLock<_redis::aio::ConnectionManager> = OnceLock::new();
129
130#[derive(Clone, Debug)]
132pub enum RullstValue {
133 String(String),
134 Int(i32),
135 Float(f64),
136 Bool(bool),
137}
138
139impl From<&str> for RullstValue {
140 fn from(s: &str) -> Self {
141 RullstValue::String(s.to_string())
142 }
143}
144impl From<String> for RullstValue {
145 fn from(s: String) -> Self {
146 RullstValue::String(s)
147 }
148}
149impl From<i32> for RullstValue {
150 fn from(i: i32) -> Self {
151 RullstValue::Int(i)
152 }
153}
154impl From<f64> for RullstValue {
155 fn from(f: f64) -> Self {
156 RullstValue::Float(f)
157 }
158}
159impl From<bool> for RullstValue {
160 fn from(b: bool) -> Self {
161 RullstValue::Bool(b)
162 }
163}
164
165impl TryFrom<RullstValue> for String {
166 type Error = &'static str;
167 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
168 match val {
169 RullstValue::String(s) => Ok(s),
170 _ => Err("Not a string"),
171 }
172 }
173}
174impl TryFrom<RullstValue> for i32 {
175 type Error = &'static str;
176 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
177 match val {
178 RullstValue::Int(i) => Ok(i),
179 _ => Err("Not an i32"),
180 }
181 }
182}
183impl TryFrom<RullstValue> for f64 {
184 type Error = &'static str;
185 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
186 match val {
187 RullstValue::Float(f) => Ok(f),
188 _ => Err("Not an f64"),
189 }
190 }
191}
192impl TryFrom<RullstValue> for bool {
193 type Error = &'static str;
194 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
195 match val {
196 RullstValue::Bool(b) => Ok(b),
197 _ => Err("Not a bool"),
198 }
199 }
200}
201
202pub struct Orm;
204
205impl Orm {
206 pub async fn init(database_url: &str) -> Result<(), crate::Error> {
208 Self::validate_dsn(database_url);
209
210 #[cfg(not(any(
211 feature = "strict-postgres",
212 feature = "strict-mysql",
213 feature = "strict-sqlite"
214 )))]
215 install_default_drivers();
216
217 let pool = RullstPool::connect(database_url).await?;
218
219 if DB_POOL.set(pool).is_err() {
220 return Err(crate::Error::Internal(
221 "Orm has already been initialized".to_string(),
222 ));
223 }
224
225 let driver = if database_url.starts_with("postgres") {
226 "postgres"
227 } else if database_url.starts_with("mysql") {
228 "mysql"
229 } else {
230 "sqlite"
231 };
232
233 let _ = DB_DRIVER.set(driver.to_string());
234 let _ = REPLICA_POOLS.set(vec![]);
235
236 Ok(())
237 }
238
239 pub async fn init_with_options(
241 database_url: &str,
242 max_connections: u32,
243 acquire_timeout_secs: u64,
244 ) -> Result<(), crate::Error> {
245 Self::validate_dsn(database_url);
246
247 #[cfg(not(any(
248 feature = "strict-postgres",
249 feature = "strict-mysql",
250 feature = "strict-sqlite"
251 )))]
252 install_default_drivers();
253
254 let pool = RullstPoolOptions::new()
255 .max_connections(max_connections)
256 .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs))
257 .connect(database_url)
258 .await?;
259
260 if DB_POOL.set(pool).is_err() {
261 return Err(crate::Error::Internal(
262 "Orm has already been initialized".to_string(),
263 ));
264 }
265
266 let driver = if database_url.starts_with("postgres") {
267 "postgres"
268 } else if database_url.starts_with("mysql") {
269 "mysql"
270 } else {
271 "sqlite"
272 };
273
274 let _ = DB_DRIVER.set(driver.to_string());
275 let _ = REPLICA_POOLS.set(vec![]);
276
277 Ok(())
278 }
279
280 #[cfg_attr(test, mutants::skip)]
281 fn validate_dsn(database_url: &str) {
282 if database_url.contains("sslmode=disable")
283 && !database_url.contains("localhost")
284 && !database_url.contains("127.0.0.1")
285 {
286 eprintln!(
287 "⚠️ [SECURITY WARNING] Rullst ORM: TLS/SSL disabled on external database connection! This is highly discouraged in production environments."
288 );
289 }
290 }
291
292 pub async fn init_with_replicas(
294 primary_url: &str,
295 replica_urls: Vec<&str>,
296 ) -> Result<(), crate::Error> {
297 #[cfg(not(any(
298 feature = "strict-postgres",
299 feature = "strict-mysql",
300 feature = "strict-sqlite"
301 )))]
302 install_default_drivers();
303
304 let pool = RullstPool::connect(primary_url).await?;
305
306 if DB_POOL.set(pool).is_err() {
307 return Err(crate::Error::Internal(
308 "Orm has already been initialized".to_string(),
309 ));
310 }
311
312 let driver = if primary_url.starts_with("postgres") {
313 "postgres"
314 } else if primary_url.starts_with("mysql") {
315 "mysql"
316 } else {
317 "sqlite"
318 };
319
320 let _ = DB_DRIVER.set(driver.to_string());
321
322 let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
324 let replicas = futures::future::try_join_all(replica_futures).await?;
325 let _ = REPLICA_POOLS.set(replicas);
326
327 Ok(())
328 }
329
330 pub fn pool() -> &'static RullstPool {
332 DB_POOL
333 .get()
334 .expect("Orm must be initialized before querying")
335 }
336
337 #[cfg_attr(test, mutants::skip)]
340 pub fn read_pool() -> &'static RullstPool {
341 if let Some(replicas) = REPLICA_POOLS.get()
342 && !replicas.is_empty()
343 {
344 let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
345 return &replicas[idx];
346 }
347 Self::pool()
348 }
349
350 pub fn driver() -> &'static str {
352 DB_DRIVER
353 .get()
354 .expect("Orm must be initialized before querying")
355 .as_str()
356 }
357
358 pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
359 let pool = Self::pool();
360 pool.begin().await.map_err(Into::into)
361 }
362
363 #[cfg_attr(test, mutants::skip)]
365 pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
366 for seeder in seeders {
367 seeder.run().await?;
368 }
369 Ok(())
370 }
371
372 pub fn enable_query_log() {
374 crate::schema::enable_query_log();
375 }
376
377 pub fn disable_query_log() {
379 crate::schema::disable_query_log();
380 }
381
382 pub fn set_max_query_limit(limit: usize) {
384 crate::schema::set_max_query_limit(limit);
385 }
386
387 pub fn set_query_timeout(secs: u64) {
389 crate::schema::set_query_timeout(secs);
390 }
391
392 #[cfg(feature = "redis")]
394 #[cfg_attr(test, mutants::skip)]
395 pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
396 let client = _redis::Client::open(redis_url)?;
397 let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
398 let _ = REDIS_CLIENT.set(client);
399 let _ = REDIS_MANAGER.set(manager);
400 Ok(())
401 }
402
403 #[cfg(feature = "redis")]
405 #[cfg_attr(test, mutants::skip)]
406 pub fn redis_client() -> Result<&'static _redis::Client, crate::Error> {
407 REDIS_CLIENT.get().ok_or_else(|| {
408 crate::Error::Internal(
409 "Orm::init_redis() must be called before using cache features".to_string(),
410 )
411 })
412 }
413
414 #[cfg(feature = "redis")]
416 #[cfg_attr(test, mutants::skip)]
417 pub fn redis_manager() -> Result<_redis::aio::ConnectionManager, crate::Error> {
418 REDIS_MANAGER.get().cloned().ok_or_else(|| {
419 crate::Error::Internal(
420 "Orm::init_redis() must be called before using cache features".to_string(),
421 )
422 })
423 }
424}
425
426#[async_trait]
428pub trait Seeder: Send + Sync {
429 async fn run(&self) -> Result<(), crate::Error>;
430}
431
432#[async_trait]
434pub trait RullstModel {
435 fn table_name() -> &'static str;
436}
437
438#[derive(Debug, Clone)]
440pub struct PaginationResult<T> {
441 pub data: Vec<T>,
442 pub total: i64,
443 pub per_page: usize,
444 pub current_page: usize,
445 pub last_page: usize,
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn test_pagination_result() {
454 let mut pr = PaginationResult {
455 data: vec![1, 2, 3],
456 total: 3,
457 per_page: 10,
458 current_page: 1,
459 last_page: 1,
460 };
461 assert_eq!(pr.data.len(), 3);
462 assert_eq!(pr.total, 3);
463 pr.data.push(4);
464 assert_eq!(pr.data.len(), 4);
465 }
466
467 #[test]
468 fn test_replace_placeholders() {
469 assert_eq!(
470 super::replace_placeholders("SELECT * FROM users WHERE id = ? AND name = ?"),
471 "SELECT * FROM users WHERE id = $1 AND name = $2"
472 );
473 assert_eq!(
474 super::replace_placeholders("INSERT INTO users (name) VALUES (?)"),
475 "INSERT INTO users (name) VALUES ($1)"
476 );
477 assert_eq!(
478 super::replace_placeholders("SELECT * FROM users"),
479 "SELECT * FROM users"
480 );
481 assert_eq!(super::replace_placeholders("? ? ?"), "$1 $2 $3");
482 }
483
484 #[test]
485 fn test_rullst_value_conversions() {
486 let v: RullstValue = "test".into();
488 assert!(matches!(v, RullstValue::String(_)));
489 let v_string: RullstValue = "test".to_string().into();
490 assert!(matches!(v_string, RullstValue::String(_)));
491 let v_int: RullstValue = 100.into();
492 assert!(matches!(v_int, RullstValue::Int(100)));
493 let v_bool: RullstValue = false.into();
494 assert!(matches!(v_bool, RullstValue::Bool(false)));
495 let v_float: RullstValue = std::f64::consts::PI.into();
496 assert!(matches!(v_float, RullstValue::Float(_)));
497
498 let v_str_conv = RullstValue::String("hello".to_string());
500 assert_eq!(String::try_from(v_str_conv).unwrap(), "hello");
501 assert!(String::try_from(RullstValue::Int(10)).is_err());
502
503 let v_int_conv = RullstValue::Int(42);
505 assert_eq!(i32::try_from(v_int_conv).unwrap(), 42);
506 assert!(i32::try_from(RullstValue::Bool(true)).is_err());
507
508 let v_float_conv = RullstValue::Float(2.71);
510 assert_eq!(f64::try_from(v_float_conv).unwrap(), 2.71);
511 assert!(f64::try_from(RullstValue::Int(10)).is_err());
512
513 let v_bool_conv = RullstValue::Bool(true);
515 assert!(bool::try_from(v_bool_conv).unwrap());
516 assert!(bool::try_from(RullstValue::Int(10)).is_err());
517 }
518
519 #[test]
520 fn test_enable_query_log_wrapper() {
521 Orm::disable_query_log();
523 assert!(!crate::schema::is_query_log_enabled());
524 Orm::enable_query_log();
525 assert!(crate::schema::is_query_log_enabled());
526 Orm::disable_query_log();
527 assert!(!crate::schema::is_query_log_enabled());
528 }
529
530 #[test]
531 fn test_disable_query_log_wrapper() {
532 Orm::enable_query_log();
533 Orm::disable_query_log();
534 assert!(!crate::schema::is_query_log_enabled());
535 }
536
537 #[cfg(feature = "redis")]
538 #[test]
539 fn test_redis_client_uninitialized() {
540 let err = Orm::redis_client().unwrap_err();
541 assert!(matches!(err, crate::Error::Internal(_)));
542 }
543
544 #[cfg(feature = "redis")]
545 #[test]
546 fn test_redis_manager_uninitialized() {
547 let err = Orm::redis_manager().unwrap_err();
548 assert!(matches!(err, crate::Error::Internal(_)));
549 }
550
551 #[test]
552 #[should_panic(expected = "Orm must be initialized before querying")]
553 fn test_pool_uninitialized() {
554 let _ = Orm::pool();
555 }
556
557 #[test]
558 #[should_panic(expected = "Orm must be initialized before querying")]
559 fn test_driver_uninitialized() {
560 let _ = Orm::driver();
561 }
562
563 #[test]
564 #[should_panic(expected = "Orm must be initialized before querying")]
565 fn test_read_pool_uninitialized() {
566 let _ = Orm::read_pool();
567 }
568
569 #[test]
570 fn test_validate_dsn() {
571 Orm::validate_dsn("sqlite::memory:");
573 Orm::validate_dsn("postgres://external-db.com/mydb?sslmode=disable");
575 }
576
577 #[cfg(feature = "redis")]
578 #[tokio::test]
579 async fn test_init_redis_failure() {
580 let err = Orm::init_redis("redis://127.0.0.1:0").await.unwrap_err();
581 assert!(matches!(err, crate::Error::CacheError(_)));
582 }
583
584 #[test]
585 fn test_orm_max_query_limit_and_timeout() {
586 Orm::set_max_query_limit(15);
587 assert_eq!(crate::schema::get_max_query_limit(), Some(15));
588 Orm::set_max_query_limit(0);
589 assert_eq!(crate::schema::get_max_query_limit(), None);
590
591 Orm::set_query_timeout(5);
592 assert_eq!(
593 crate::schema::get_query_timeout(),
594 Some(std::time::Duration::from_secs(5))
595 );
596 Orm::set_query_timeout(0);
597 assert_eq!(crate::schema::get_query_timeout(), None);
598 }
599}