Skip to main content

runlight_sqlx/
postgres.rs

1//! Postgres, on the app's pool. Each statement runs on the pool on its own, so the store's writes never
2//! join a transaction the app has open; a transaction or a lock holds one connection until it ends.
3
4use std::sync::Arc;
5
6use runlight::BoxFuture;
7use runlight::store::{Db, DbError, Dialect, Hold, Param, Row, fill_placeholders, postgres_literal};
8use sqlx::pool::PoolConnection;
9use sqlx::{AssertSqlSafe, PgPool, Postgres};
10use tokio::sync::Mutex;
11
12use crate::rows::{db_error, pg_row};
13
14/// Arbitrary but fixed, so every Runlight process takes the same lock to create tables.
15const MIGRATION_LOCK: i64 = 7_331_906;
16
17/// The store's driver over a Postgres pool. Values go in the statement's text as literals of unknown
18/// type, so Postgres infers each one's type as it does for node-postgres's untyped parameters.
19#[derive(Clone)]
20pub struct PostgresDb {
21    pool: PgPool,
22}
23
24impl PostgresDb {
25    /// The store's driver over a pool.
26    pub fn new(pool: PgPool) -> PostgresDb {
27        PostgresDb { pool }
28    }
29}
30
31fn text(sql: &str, params: &[Param]) -> Result<AssertSqlSafe<String>, DbError> {
32    Ok(AssertSqlSafe(fill_placeholders(sql, params, false, postgres_literal)?))
33}
34
35impl Db for PostgresDb {
36    fn dialect(&self) -> Dialect {
37        Dialect::Postgres
38    }
39
40    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
41        Box::pin(async move {
42            let rows = sqlx::raw_sql(text(sql, &params)?).fetch_all(&self.pool).await.map_err(db_error)?;
43            rows.iter().map(pg_row).collect()
44        })
45    }
46
47    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
48        Box::pin(async move {
49            Ok(sqlx::raw_sql(text(sql, &params)?).execute(&self.pool).await.map_err(db_error)?.rows_affected())
50        })
51    }
52
53    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
54        Box::pin(async move {
55            let mut conn = self.pool.acquire().await.map_err(db_error)?;
56            match kind {
57                Hold::Transaction => {
58                    sqlx::raw_sql(AssertSqlSafe("BEGIN".to_string())).execute(&mut *conn).await.map_err(db_error)?;
59                }
60                Hold::Exclusive => loop {
61                    // Asked for again and again rather than waited on: a waiting statement would hold up an
62                    // index being built CONCURRENTLY by whoever has the lock, and the two would wait on each
63                    // other for good.
64                    let rows =
65                        sqlx::raw_sql(AssertSqlSafe(format!("SELECT pg_try_advisory_lock({MIGRATION_LOCK}) AS ok")))
66                            .fetch_all(&mut *conn)
67                            .await
68                            .map_err(db_error)?;
69                    let ok = rows.first().map(pg_row).transpose()?.is_some_and(|r| r.num("ok") == 1.0);
70                    if ok {
71                        break;
72                    }
73                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
74                },
75            }
76            Ok(Arc::new(Held { conn: Arc::new(Mutex::new(Some(conn))), kind, role: Role::Owner }) as Arc<dyn Db>)
77        })
78    }
79}
80
81/// One connection held for a transaction or a lock.
82struct Held {
83    conn: Arc<Mutex<Option<PoolConnection<Postgres>>>>,
84    kind: Hold,
85    role: Role,
86}
87
88/// What finishing a held connection does.
89#[derive(Clone, Copy, PartialEq, Eq)]
90enum Role {
91    /// Ends the transaction or lets the lock go, and gives the connection back.
92    Owner,
93    /// A transaction begun inside a lock: ends the transaction, and keeps the connection.
94    Nested,
95    /// Joined what was already held: ends nothing.
96    Joined,
97}
98
99fn ended() -> DbError {
100    DbError("Runlight: the held connection has been let go".into())
101}
102
103impl Db for Held {
104    fn dialect(&self) -> Dialect {
105        Dialect::Postgres
106    }
107
108    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
109        Box::pin(async move {
110            let mut guard = self.conn.lock().await;
111            let conn = guard.as_mut().ok_or_else(ended)?;
112            let rows = sqlx::raw_sql(text(sql, &params)?).fetch_all(&mut **conn).await.map_err(db_error)?;
113            rows.iter().map(pg_row).collect()
114        })
115    }
116
117    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
118        Box::pin(async move {
119            let mut guard = self.conn.lock().await;
120            let conn = guard.as_mut().ok_or_else(ended)?;
121            Ok(sqlx::raw_sql(text(sql, &params)?).execute(&mut **conn).await.map_err(db_error)?.rows_affected())
122        })
123    }
124
125    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
126        Box::pin(async move {
127            // A transaction inside the lock runs on the locked connection; anything else joins what is held.
128            if kind == Hold::Transaction && self.kind == Hold::Exclusive {
129                let mut guard = self.conn.lock().await;
130                let conn = guard.as_mut().ok_or_else(ended)?;
131                sqlx::raw_sql(AssertSqlSafe("BEGIN".to_string())).execute(&mut **conn).await.map_err(db_error)?;
132                return Ok(Arc::new(Held { conn: self.conn.clone(), kind, role: Role::Nested }) as Arc<dyn Db>);
133            }
134            Ok(Arc::new(Held { conn: self.conn.clone(), kind: self.kind, role: Role::Joined }) as Arc<dyn Db>)
135        })
136    }
137
138    fn finish(&self, ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
139        Box::pin(async move {
140            let mut guard = self.conn.lock().await;
141            if self.role == Role::Joined {
142                return Ok(());
143            }
144            if self.role == Role::Nested {
145                let Some(conn) = guard.as_mut() else { return Ok(()) };
146                let end = if ok { "COMMIT" } else { "ROLLBACK" };
147                sqlx::raw_sql(AssertSqlSafe(end.to_string())).execute(&mut **conn).await.map_err(db_error)?;
148                return Ok(());
149            }
150            let Some(mut conn) = guard.take() else { return Ok(()) };
151            let result = match self.kind {
152                Hold::Transaction => {
153                    let end = if ok { "COMMIT" } else { "ROLLBACK" };
154                    sqlx::raw_sql(AssertSqlSafe(end.to_string()))
155                        .execute(&mut *conn)
156                        .await
157                        .map(|_| ())
158                        .map_err(db_error)
159                }
160                // A lost connection ends its session, and the lock with it.
161                Hold::Exclusive => {
162                    let _ = sqlx::raw_sql(AssertSqlSafe(format!("SELECT pg_advisory_unlock({MIGRATION_LOCK})")))
163                        .execute(&mut *conn)
164                        .await;
165                    Ok(())
166                }
167            };
168            if result.is_err() {
169                // A connection still in its transaction must never go back to the pool.
170                conn.detach();
171            }
172            result
173        })
174    }
175}