chronon_backend_sql_common/
backend.rs1use std::fmt;
2
3use chronon_core::Result;
4use sqlx::{Executor, Pool, Postgres, Sqlite};
5
6use crate::error_map::map_err;
7use crate::schema;
8
9#[must_use]
11pub fn postgres_max_connections() -> u32 {
12 std::env::var("CHRONON_PG_POOL_SIZE")
13 .ok()
14 .and_then(|v| v.parse::<u32>().ok())
15 .unwrap_or(5)
16 .clamp(1, 200)
17}
18
19pub fn bind_sql(dialect: SqlDialect, sql: &str) -> String {
21 match dialect {
22 SqlDialect::Sqlite => sql.to_string(),
23 SqlDialect::Postgres => {
24 let mut out = String::with_capacity(sql.len());
25 let mut n = 1u32;
26 for ch in sql.chars() {
27 if ch == '?' {
28 out.push('$');
29 out.push_str(&n.to_string());
30 n += 1;
31 } else {
32 out.push(ch);
33 }
34 }
35 out
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SqlDialect {
43 Postgres,
45 Sqlite,
47}
48
49#[derive(Clone)]
51pub enum SqlPool {
52 Sqlite(Pool<Sqlite>),
54 Postgres(Pool<Postgres>),
56}
57
58impl fmt::Debug for SqlPool {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::Sqlite(_) => f.debug_tuple("SqlPool::Sqlite").finish(),
62 Self::Postgres(_) => f.debug_tuple("SqlPool::Postgres").finish(),
63 }
64 }
65}
66
67pub struct SqlSchedulerStore {
69 pub(crate) pool: SqlPool,
70 pub(crate) dialect: SqlDialect,
71}
72
73impl SqlSchedulerStore {
74 pub async fn connect_sqlite(url: &str) -> Result<Self> {
80 let pool = sqlx::sqlite::SqlitePoolOptions::new()
81 .max_connections(5)
82 .connect(url)
83 .await
84 .map_err(|e| map_err(&e))?;
85 Self::from_sqlite_pool(pool).await
86 }
87
88 pub async fn connect_postgres(url: &str) -> Result<Self> {
94 let pool = sqlx::postgres::PgPoolOptions::new()
95 .max_connections(postgres_max_connections())
96 .connect(url)
97 .await
98 .map_err(|e| map_err(&e))?;
99 Self::from_postgres_pool(pool).await
100 }
101
102 pub async fn connect_postgres_isolated(url: &str, schema: &str) -> Result<Self> {
108 let admin = sqlx::postgres::PgPoolOptions::new()
109 .max_connections(1)
110 .connect(url)
111 .await
112 .map_err(|e| map_err(&e))?;
113 let ddl = format!("CREATE SCHEMA IF NOT EXISTS \"{schema}\"");
114 admin.execute(ddl.as_str()).await.map_err(|e| map_err(&e))?;
115 drop(admin);
116
117 let schema = schema.to_string();
118 let pool = sqlx::postgres::PgPoolOptions::new()
119 .max_connections(postgres_max_connections())
120 .after_connect(move |conn, _meta| {
121 let schema = schema.clone();
122 Box::pin(async move {
123 let sql = format!("SET search_path TO \"{schema}\"");
124 sqlx::query(&sql).execute(conn).await?;
125 Ok(())
126 })
127 })
128 .connect(url)
129 .await
130 .map_err(|e| map_err(&e))?;
131 Self::from_postgres_pool(pool).await
132 }
133
134 pub async fn attach_postgres_isolated(url: &str, schema: &str) -> Result<Self> {
142 let schema = schema.to_string();
143 let pool = sqlx::postgres::PgPoolOptions::new()
144 .max_connections(postgres_max_connections())
145 .after_connect(move |conn, _meta| {
146 let schema = schema.clone();
147 Box::pin(async move {
148 let sql = format!("SET search_path TO \"{schema}\"");
149 sqlx::query(&sql).execute(conn).await?;
150 Ok(())
151 })
152 })
153 .connect(url)
154 .await
155 .map_err(|e| map_err(&e))?;
156 Ok(Self {
157 pool: SqlPool::Postgres(pool),
158 dialect: SqlDialect::Postgres,
159 })
160 }
161
162 pub async fn from_sqlite_pool(pool: Pool<Sqlite>) -> Result<Self> {
168 let store = Self {
169 pool: SqlPool::Sqlite(pool),
170 dialect: SqlDialect::Sqlite,
171 };
172 schema::ensure_schema(&store).await?;
173 Ok(store)
174 }
175
176 pub async fn from_postgres_pool(pool: Pool<Postgres>) -> Result<Self> {
182 let store = Self {
183 pool: SqlPool::Postgres(pool),
184 dialect: SqlDialect::Postgres,
185 };
186 schema::ensure_schema(&store).await?;
187 Ok(store)
188 }
189
190 #[must_use]
192 pub const fn pool(&self) -> &SqlPool {
193 &self.pool
194 }
195
196 #[must_use]
198 pub const fn dialect(&self) -> SqlDialect {
199 self.dialect
200 }
201
202 pub(crate) async fn run_ddl(&self, ddl: &str) -> Result<()> {
203 match &self.pool {
204 SqlPool::Sqlite(pool) => {
205 pool.execute(ddl).await.map_err(|e| map_err(&e))?;
206 }
207 SqlPool::Postgres(pool) => {
208 pool.execute(ddl).await.map_err(|e| map_err(&e))?;
209 }
210 }
211 Ok(())
212 }
213
214 pub async fn drop_postgres_schema(url: &str, schema: &str) -> Result<()> {
220 let admin = sqlx::postgres::PgPoolOptions::new()
221 .max_connections(1)
222 .connect(url)
223 .await
224 .map_err(|e| map_err(&e))?;
225 let ddl = format!("DROP SCHEMA IF EXISTS \"{schema}\" CASCADE");
226 admin.execute(ddl.as_str()).await.map_err(|e| map_err(&e))?;
227 Ok(())
228 }
229}
230
231impl fmt::Debug for SqlSchedulerStore {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 f.debug_struct("SqlSchedulerStore")
234 .field("dialect", &self.dialect)
235 .finish_non_exhaustive()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::{bind_sql, SqlDialect};
242
243 #[test]
244 fn bind_sql_sqlite_passthrough() {
245 let sql = "SELECT * FROM t WHERE id = ? AND name = ?";
246 assert_eq!(bind_sql(SqlDialect::Sqlite, sql), sql);
247 }
248
249 #[test]
250 fn bind_sql_postgres_renumbers_placeholders() {
251 let sql = "UPDATE t SET a = ?, b = ? WHERE id = ?";
252 assert_eq!(
253 bind_sql(SqlDialect::Postgres, sql),
254 "UPDATE t SET a = $1, b = $2 WHERE id = $3"
255 );
256 }
257}