1#[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
2pub use sqlx::AnyPool as EloquentPool;
3
4#[cfg(feature = "strict-postgres")]
5pub use sqlx::PgPool as EloquentPool;
6
7#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
8pub use sqlx::MySqlPool as EloquentPool;
9
10#[cfg(all(feature = "strict-sqlite", not(feature = "strict-postgres"), not(feature = "strict-mysql")))]
11pub use sqlx::SqlitePool as EloquentPool;
12
13#[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
14use sqlx::any::install_default_drivers;
15
16use std::sync::OnceLock;
17use std::sync::atomic::{AtomicUsize, Ordering};
18
19pub use rullst_orm_macros::*;
21pub use sqlx;
22pub use futures;
23pub use serde;
24pub use serde_json;
25
26#[cfg(feature = "redis")]
27pub use redis;
28pub mod schema;
29pub mod collection;
30pub mod types;
31pub mod database;
32
33pub use types::Json;
34pub use collection::EloquentCollection;
35pub use database::EloquentDatabase;
36
37pub use async_trait::async_trait;
39
40pub use sqlx::FromRow;
42pub use schema::{JoinClause, SubqueryBuilder};
43
44static DB_POOL: OnceLock<EloquentPool> = OnceLock::new();
46
47static DB_DRIVER: OnceLock<String> = OnceLock::new();
49
50static REPLICA_POOLS: OnceLock<Vec<EloquentPool>> = OnceLock::new();
52
53static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
55
56#[cfg(feature = "redis")]
57static REDIS_CLIENT: OnceLock<redis::Client> = OnceLock::new();
58
59#[cfg(feature = "redis")]
60static REDIS_MANAGER: OnceLock<redis::aio::ConnectionManager> = OnceLock::new();
61
62#[derive(Clone, Debug)]
64pub enum EloquentValue {
65 String(String),
66 Int(i32),
67 Float(f64),
68 Bool(bool),
69}
70
71impl From<&str> for EloquentValue {
72 fn from(s: &str) -> Self { EloquentValue::String(s.to_string()) }
73}
74impl From<String> for EloquentValue {
75 fn from(s: String) -> Self { EloquentValue::String(s) }
76}
77impl From<i32> for EloquentValue {
78 fn from(i: i32) -> Self { EloquentValue::Int(i) }
79}
80impl From<f64> for EloquentValue {
81 fn from(f: f64) -> Self { EloquentValue::Float(f) }
82}
83impl From<bool> for EloquentValue {
84 fn from(b: bool) -> Self { EloquentValue::Bool(b) }
85}
86
87pub struct Orm;
89
90impl Orm {
91 pub async fn init(database_url: &str) -> Result<(), sqlx::Error> {
93 #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
94 install_default_drivers();
95
96 let pool = EloquentPool::connect(database_url).await?;
97
98 if DB_POOL.set(pool).is_err() {
99 panic!("Orm has already been initialized");
100 }
101
102 let driver = if database_url.starts_with("postgres") {
103 "postgres"
104 } else if database_url.starts_with("mysql") {
105 "mysql"
106 } else {
107 "sqlite"
108 };
109
110 let _ = DB_DRIVER.set(driver.to_string());
111 let _ = REPLICA_POOLS.set(vec![]);
112
113 Ok(())
114 }
115
116 pub async fn init_with_replicas(primary_url: &str, replica_urls: Vec<&str>) -> Result<(), sqlx::Error> {
118 #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
119 install_default_drivers();
120
121 let pool = EloquentPool::connect(primary_url).await?;
122
123 if DB_POOL.set(pool).is_err() {
124 panic!("Orm has already been initialized");
125 }
126
127 let driver = if primary_url.starts_with("postgres") {
128 "postgres"
129 } else if primary_url.starts_with("mysql") {
130 "mysql"
131 } else {
132 "sqlite"
133 };
134
135 let _ = DB_DRIVER.set(driver.to_string());
136
137 let mut replicas = vec![];
138 for url in replica_urls {
139 let p = EloquentPool::connect(url).await?;
140 replicas.push(p);
141 }
142 let _ = REPLICA_POOLS.set(replicas);
143
144 Ok(())
145 }
146
147 pub fn pool() -> &'static EloquentPool {
149 DB_POOL.get().expect("Orm must be initialized before querying")
150 }
151
152 pub fn read_pool() -> &'static EloquentPool {
155 if let Some(replicas) = REPLICA_POOLS.get()
156 && !replicas.is_empty() {
157 let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
158 return &replicas[idx];
159 }
160 Self::pool()
161 }
162
163 pub fn driver() -> &'static str {
165 DB_DRIVER.get().expect("Orm must be initialized before querying").as_str()
166 }
167
168 #[cfg(not(any(feature = "strict-postgres", feature = "strict-mysql", feature = "strict-sqlite")))]
170 pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Any>, sqlx::Error> {
171 let pool = Self::pool();
172 pool.begin().await
173 }
174
175 #[cfg(feature = "strict-postgres")]
176 pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Postgres>, sqlx::Error> {
177 let pool = Self::pool();
178 pool.begin().await
179 }
180
181 #[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
182 pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::MySql>, sqlx::Error> {
183 let pool = Self::pool();
184 pool.begin().await
185 }
186
187 #[cfg(all(feature = "strict-sqlite", not(feature = "strict-postgres"), not(feature = "strict-mysql")))]
188 pub async fn begin_transaction() -> Result<sqlx::Transaction<'static, sqlx::Sqlite>, sqlx::Error> {
189 let pool = Self::pool();
190 pool.begin().await
191 }
192
193 pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), sqlx::Error> {
195 for seeder in seeders {
196 seeder.run().await?;
197 }
198 Ok(())
199 }
200
201 pub fn enable_query_log() {
203 crate::schema::enable_query_log();
204 }
205
206 pub fn disable_query_log() {
208 crate::schema::disable_query_log();
209 }
210
211 #[cfg(feature = "redis")]
213 pub async fn init_redis(redis_url: &str) -> Result<(), redis::RedisError> {
214 let client = redis::Client::open(redis_url)?;
215 let manager = redis::aio::ConnectionManager::new(client.clone()).await?;
216 let _ = REDIS_CLIENT.set(client);
217 let _ = REDIS_MANAGER.set(manager);
218 Ok(())
219 }
220
221 #[cfg(feature = "redis")]
223 pub fn redis_client() -> &'static redis::Client {
224 REDIS_CLIENT.get().expect("Redis must be initialized before using cache features")
225 }
226
227 #[cfg(feature = "redis")]
229 pub fn redis_manager() -> redis::aio::ConnectionManager {
230 REDIS_MANAGER.get().expect("Redis must be initialized before using cache features").clone()
231 }
232}
233
234#[async_trait]
236pub trait Seeder: Send + Sync {
237 async fn run(&self) -> Result<(), sqlx::Error>;
238}
239
240#[async_trait]
242pub trait EloquentModel {
243 fn table_name() -> &'static str;
244}
245
246#[derive(Debug, Clone)]
248pub struct PaginationResult<T> {
249 pub data: Vec<T>,
250 pub total: i64,
251 pub per_page: usize,
252 pub current_page: usize,
253 pub last_page: usize,
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_eloquent_value_conversions() {
262 let v: EloquentValue = "test".into();
263 assert!(matches!(v, EloquentValue::String(_)));
264 let v_int: EloquentValue = 100.into();
265 assert!(matches!(v_int, EloquentValue::Int(100)));
266 let v_bool: EloquentValue = false.into();
267 assert!(matches!(v_bool, EloquentValue::Bool(false)));
268 }
269}