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 panic!("Orm has already been initialized");
183 }
184
185 let driver = if database_url.starts_with("postgres") {
186 "postgres"
187 } else if database_url.starts_with("mysql") {
188 "mysql"
189 } else {
190 "sqlite"
191 };
192
193 let _ = DB_DRIVER.set(driver.to_string());
194 let _ = REPLICA_POOLS.set(vec![]);
195
196 Ok(())
197 }
198
199 pub async fn init_with_replicas(
201 primary_url: &str,
202 replica_urls: Vec<&str>,
203 ) -> Result<(), crate::Error> {
204 #[cfg(not(any(
205 feature = "strict-postgres",
206 feature = "strict-mysql",
207 feature = "strict-sqlite"
208 )))]
209 install_default_drivers();
210
211 let pool = RullstPool::connect(primary_url).await?;
212
213 if DB_POOL.set(pool).is_err() {
214 panic!("Orm has already been initialized");
215 }
216
217 let driver = if primary_url.starts_with("postgres") {
218 "postgres"
219 } else if primary_url.starts_with("mysql") {
220 "mysql"
221 } else {
222 "sqlite"
223 };
224
225 let _ = DB_DRIVER.set(driver.to_string());
226
227 let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
229 let replicas = futures::future::try_join_all(replica_futures).await?;
230 let _ = REPLICA_POOLS.set(replicas);
231
232 Ok(())
233 }
234
235 pub fn pool() -> &'static RullstPool {
237 DB_POOL
238 .get()
239 .expect("Orm must be initialized before querying")
240 }
241
242 pub fn read_pool() -> &'static RullstPool {
245 if let Some(replicas) = REPLICA_POOLS.get()
246 && !replicas.is_empty()
247 {
248 let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
249 return &replicas[idx];
250 }
251 Self::pool()
252 }
253
254 pub fn driver() -> &'static str {
256 DB_DRIVER
257 .get()
258 .expect("Orm must be initialized before querying")
259 .as_str()
260 }
261
262 pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
263 let pool = Self::pool();
264 pool.begin().await.map_err(Into::into)
265 }
266
267 pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
269 for seeder in seeders {
270 seeder.run().await?;
271 }
272 Ok(())
273 }
274
275 pub fn enable_query_log() {
277 crate::schema::enable_query_log();
278 }
279
280 pub fn disable_query_log() {
282 crate::schema::disable_query_log();
283 }
284
285 #[cfg(feature = "redis")]
287 pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
288 let client = _redis::Client::open(redis_url)?;
289 let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
290 let _ = REDIS_CLIENT.set(client);
291 let _ = REDIS_MANAGER.set(manager);
292 Ok(())
293 }
294
295 #[cfg(feature = "redis")]
297 pub fn redis_client() -> &'static _redis::Client {
298 REDIS_CLIENT
299 .get()
300 .expect("Redis must be initialized before using cache features")
301 }
302
303 #[cfg(feature = "redis")]
305 pub fn redis_manager() -> _redis::aio::ConnectionManager {
306 REDIS_MANAGER
307 .get()
308 .expect("Redis must be initialized before using cache features")
309 .clone()
310 }
311}
312
313#[async_trait]
315pub trait Seeder: Send + Sync {
316 async fn run(&self) -> Result<(), crate::Error>;
317}
318
319#[async_trait]
321pub trait RullstModel {
322 fn table_name() -> &'static str;
323}
324
325#[derive(Debug, Clone)]
327pub struct PaginationResult<T> {
328 pub data: Vec<T>,
329 pub total: i64,
330 pub per_page: usize,
331 pub current_page: usize,
332 pub last_page: usize,
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_rullst_value_conversions() {
341 let v: RullstValue = "test".into();
342 assert!(matches!(v, RullstValue::String(_)));
343 let v_int: RullstValue = 100.into();
344 assert!(matches!(v_int, RullstValue::Int(100)));
345 let v_bool: RullstValue = false.into();
346 assert!(matches!(v_bool, RullstValue::Bool(false)));
347 }
348
349 #[test]
350 fn test_enable_query_log_wrapper() {
351 Orm::disable_query_log();
353 assert!(!crate::schema::is_query_log_enabled());
354 Orm::enable_query_log();
355 assert!(crate::schema::is_query_log_enabled());
356 Orm::disable_query_log();
357 assert!(!crate::schema::is_query_log_enabled());
358 }
359
360 #[test]
361 fn test_disable_query_log_wrapper() {
362 Orm::enable_query_log();
363 Orm::disable_query_log();
364 assert!(!crate::schema::is_query_log_enabled());
365 }
366}