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;
64pub mod admin;
65pub mod audit;
66pub mod collection;
67pub mod database;
68pub mod db;
69pub mod error;
70pub mod resource;
71pub mod schema;
72pub mod scout;
73pub mod tenant;
74pub mod types;
75
76pub use error::RullstError as Error;
78
79pub use _sqlx::FromRow;
81pub use admin::dashboard_html;
82pub use collection::RullstCollection;
83pub use database::RullstDatabase;
84pub use resource::{ApiResource, JsonResource, ResourceCollection};
85pub use rullst_orm_macros::Orm;
86pub use scout::{SearchEngine, get_search_engine, set_search_engine};
87pub use tenant::{get_tenant_id, with_tenant};
88pub use types::Json;
89
90pub use async_trait::async_trait;
92
93pub use schema::{JoinClause, SubqueryBuilder};
95
96static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
98
99static DB_DRIVER: OnceLock<String> = OnceLock::new();
101
102static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
104
105static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
107
108#[cfg(feature = "redis")]
109static REDIS_CLIENT: OnceLock<_redis::Client> = OnceLock::new();
110
111#[cfg(feature = "redis")]
112static REDIS_MANAGER: OnceLock<_redis::aio::ConnectionManager> = OnceLock::new();
113
114#[derive(Clone, Debug)]
116pub enum RullstValue {
117 String(String),
118 Int(i32),
119 Float(f64),
120 Bool(bool),
121}
122
123impl From<&str> for RullstValue {
124 fn from(s: &str) -> Self {
125 RullstValue::String(s.to_string())
126 }
127}
128impl From<String> for RullstValue {
129 fn from(s: String) -> Self {
130 RullstValue::String(s)
131 }
132}
133impl From<i32> for RullstValue {
134 fn from(i: i32) -> Self {
135 RullstValue::Int(i)
136 }
137}
138impl From<f64> for RullstValue {
139 fn from(f: f64) -> Self {
140 RullstValue::Float(f)
141 }
142}
143impl From<bool> for RullstValue {
144 fn from(b: bool) -> Self {
145 RullstValue::Bool(b)
146 }
147}
148
149impl TryFrom<RullstValue> for String {
150 type Error = &'static str;
151 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
152 match val {
153 RullstValue::String(s) => Ok(s),
154 _ => Err("Not a string"),
155 }
156 }
157}
158impl TryFrom<RullstValue> for i32 {
159 type Error = &'static str;
160 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
161 match val {
162 RullstValue::Int(i) => Ok(i),
163 _ => Err("Not an i32"),
164 }
165 }
166}
167impl TryFrom<RullstValue> for f64 {
168 type Error = &'static str;
169 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
170 match val {
171 RullstValue::Float(f) => Ok(f),
172 _ => Err("Not an f64"),
173 }
174 }
175}
176impl TryFrom<RullstValue> for bool {
177 type Error = &'static str;
178 fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
179 match val {
180 RullstValue::Bool(b) => Ok(b),
181 _ => Err("Not a bool"),
182 }
183 }
184}
185
186pub struct Orm;
188
189impl Orm {
190 pub async fn init(database_url: &str) -> Result<(), crate::Error> {
192 Self::validate_dsn(database_url);
193
194 #[cfg(not(any(
195 feature = "strict-postgres",
196 feature = "strict-mysql",
197 feature = "strict-sqlite"
198 )))]
199 install_default_drivers();
200
201 let pool = RullstPool::connect(database_url).await?;
202
203 if DB_POOL.set(pool).is_err() {
204 return Err(crate::Error::Internal(
205 "Orm has already been initialized".to_string(),
206 ));
207 }
208
209 let driver = if database_url.starts_with("postgres") {
210 "postgres"
211 } else if database_url.starts_with("mysql") {
212 "mysql"
213 } else {
214 "sqlite"
215 };
216
217 let _ = DB_DRIVER.set(driver.to_string());
218 let _ = REPLICA_POOLS.set(vec![]);
219
220 Ok(())
221 }
222
223 pub async fn init_with_options(
225 database_url: &str,
226 max_connections: u32,
227 acquire_timeout_secs: u64,
228 ) -> Result<(), crate::Error> {
229 Self::validate_dsn(database_url);
230
231 #[cfg(not(any(
232 feature = "strict-postgres",
233 feature = "strict-mysql",
234 feature = "strict-sqlite"
235 )))]
236 install_default_drivers();
237
238 let pool = RullstPoolOptions::new()
239 .max_connections(max_connections)
240 .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs))
241 .connect(database_url)
242 .await?;
243
244 if DB_POOL.set(pool).is_err() {
245 return Err(crate::Error::Internal(
246 "Orm has already been initialized".to_string(),
247 ));
248 }
249
250 let driver = if database_url.starts_with("postgres") {
251 "postgres"
252 } else if database_url.starts_with("mysql") {
253 "mysql"
254 } else {
255 "sqlite"
256 };
257
258 let _ = DB_DRIVER.set(driver.to_string());
259 let _ = REPLICA_POOLS.set(vec![]);
260
261 Ok(())
262 }
263
264 fn validate_dsn(database_url: &str) {
265 if database_url.contains("sslmode=disable")
266 && !database_url.contains("localhost")
267 && !database_url.contains("127.0.0.1")
268 {
269 eprintln!(
270 "⚠️ [SECURITY WARNING] Rullst ORM: TLS/SSL disabled on external database connection! This is highly discouraged in production environments."
271 );
272 }
273 }
274
275 pub async fn init_with_replicas(
277 primary_url: &str,
278 replica_urls: Vec<&str>,
279 ) -> Result<(), crate::Error> {
280 #[cfg(not(any(
281 feature = "strict-postgres",
282 feature = "strict-mysql",
283 feature = "strict-sqlite"
284 )))]
285 install_default_drivers();
286
287 let pool = RullstPool::connect(primary_url).await?;
288
289 if DB_POOL.set(pool).is_err() {
290 return Err(crate::Error::Internal(
291 "Orm has already been initialized".to_string(),
292 ));
293 }
294
295 let driver = if primary_url.starts_with("postgres") {
296 "postgres"
297 } else if primary_url.starts_with("mysql") {
298 "mysql"
299 } else {
300 "sqlite"
301 };
302
303 let _ = DB_DRIVER.set(driver.to_string());
304
305 let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
307 let replicas = futures::future::try_join_all(replica_futures).await?;
308 let _ = REPLICA_POOLS.set(replicas);
309
310 Ok(())
311 }
312
313 pub fn pool() -> &'static RullstPool {
315 DB_POOL
316 .get()
317 .expect("Orm must be initialized before querying")
318 }
319
320 pub fn read_pool() -> &'static RullstPool {
323 if let Some(replicas) = REPLICA_POOLS.get()
324 && !replicas.is_empty()
325 {
326 let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
327 return &replicas[idx];
328 }
329 Self::pool()
330 }
331
332 pub fn driver() -> &'static str {
334 DB_DRIVER
335 .get()
336 .expect("Orm must be initialized before querying")
337 .as_str()
338 }
339
340 pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
341 let pool = Self::pool();
342 pool.begin().await.map_err(Into::into)
343 }
344
345 pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
347 for seeder in seeders {
348 seeder.run().await?;
349 }
350 Ok(())
351 }
352
353 pub fn enable_query_log() {
355 crate::schema::enable_query_log();
356 }
357
358 pub fn disable_query_log() {
360 crate::schema::disable_query_log();
361 }
362
363 pub fn set_max_query_limit(limit: usize) {
365 crate::schema::set_max_query_limit(limit);
366 }
367
368 pub fn set_query_timeout(secs: u64) {
370 crate::schema::set_query_timeout(secs);
371 }
372
373 #[cfg(feature = "redis")]
375 pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
376 let client = _redis::Client::open(redis_url)?;
377 let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
378 let _ = REDIS_CLIENT.set(client);
379 let _ = REDIS_MANAGER.set(manager);
380 Ok(())
381 }
382
383 #[cfg(feature = "redis")]
385 pub fn redis_client() -> Result<&'static _redis::Client, crate::Error> {
386 REDIS_CLIENT.get().ok_or_else(|| {
387 crate::Error::Internal(
388 "Orm::init_redis() must be called before using cache features".to_string(),
389 )
390 })
391 }
392
393 #[cfg(feature = "redis")]
395 pub fn redis_manager() -> Result<_redis::aio::ConnectionManager, crate::Error> {
396 REDIS_MANAGER.get().cloned().ok_or_else(|| {
397 crate::Error::Internal(
398 "Orm::init_redis() must be called before using cache features".to_string(),
399 )
400 })
401 }
402}
403
404#[async_trait]
406pub trait Seeder: Send + Sync {
407 async fn run(&self) -> Result<(), crate::Error>;
408}
409
410#[async_trait]
412pub trait RullstModel {
413 fn table_name() -> &'static str;
414}
415
416#[derive(Debug, Clone)]
418pub struct PaginationResult<T> {
419 pub data: Vec<T>,
420 pub total: i64,
421 pub per_page: usize,
422 pub current_page: usize,
423 pub last_page: usize,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_pagination_result() {
432 let mut pr = PaginationResult {
433 data: vec![1, 2, 3],
434 total: 3,
435 per_page: 10,
436 current_page: 1,
437 last_page: 1,
438 };
439 assert_eq!(pr.data.len(), 3);
440 assert_eq!(pr.total, 3);
441 pr.data.push(4);
442 assert_eq!(pr.data.len(), 4);
443 }
444
445 #[test]
446 fn test_rullst_value_conversions() {
447 let v: RullstValue = "test".into();
448 assert!(matches!(v, RullstValue::String(_)));
449 let v_int: RullstValue = 100.into();
450 assert!(matches!(v_int, RullstValue::Int(100)));
451 let v_bool: RullstValue = false.into();
452 assert!(matches!(v_bool, RullstValue::Bool(false)));
453 }
454
455 #[test]
456 fn test_enable_query_log_wrapper() {
457 Orm::disable_query_log();
459 assert!(!crate::schema::is_query_log_enabled());
460 Orm::enable_query_log();
461 assert!(crate::schema::is_query_log_enabled());
462 Orm::disable_query_log();
463 assert!(!crate::schema::is_query_log_enabled());
464 }
465
466 #[test]
467 fn test_disable_query_log_wrapper() {
468 Orm::enable_query_log();
469 Orm::disable_query_log();
470 assert!(!crate::schema::is_query_log_enabled());
471 }
472
473 #[cfg(feature = "redis")]
474 #[test]
475 fn test_redis_client_uninitialized() {
476 let err = Orm::redis_client().unwrap_err();
477 assert!(matches!(err, crate::Error::Internal(_)));
478 }
479
480 #[cfg(feature = "redis")]
481 #[test]
482 fn test_redis_manager_uninitialized() {
483 let err = Orm::redis_manager().unwrap_err();
484 assert!(matches!(err, crate::Error::Internal(_)));
485 }
486}