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#[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
56pub use error::RullstError as Error;
58
59pub 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
70pub use async_trait::async_trait;
72
73pub use schema::{JoinClause, SubqueryBuilder};
75
76static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
78
79static DB_DRIVER: OnceLock<String> = OnceLock::new();
81
82static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
84
85static 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#[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
166pub struct Orm;
168
169impl Orm {
170 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 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 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 pub fn pool() -> &'static RullstPool {
241 DB_POOL
242 .get()
243 .expect("Orm must be initialized before querying")
244 }
245
246 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 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 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 pub fn enable_query_log() {
281 crate::schema::enable_query_log();
282 }
283
284 pub fn disable_query_log() {
286 crate::schema::disable_query_log();
287 }
288
289 #[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 #[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 #[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#[async_trait]
322pub trait Seeder: Send + Sync {
323 async fn run(&self) -> Result<(), crate::Error>;
324}
325
326#[async_trait]
328pub trait RullstModel {
329 fn table_name() -> &'static str;
330}
331
332#[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::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}