Skip to main content

spg_sqlx/
options.rs

1//! v7.16.0 — `sqlx::ConnectOptions` for SPG.
2//!
3//! URL scheme: `spg:` followed by either `memory` (or empty
4//! path → in-memory) or a file path:
5//!
6//!   `spg:memory`            — in-memory database
7//!   `spg:///tmp/app.db`     — file-backed, absolute path
8//!   `spg:./relative.db`     — file-backed, relative path
9//!
10//! Future v7.16.x: TCP fallback for cases where a process
11//! wants to talk to an existing spg-server via sqlx (the
12//! adapter currently always opens an in-process Database, so
13//! the URL is effectively a `Database::open_path` shortcut).
14
15use std::path::PathBuf;
16use std::str::FromStr;
17use std::time::Duration;
18
19use futures_core::future::BoxFuture;
20use log::LevelFilter;
21use sqlx_core::connection::{ConnectOptions, LogSettings};
22use sqlx_core::error::Error;
23
24use crate::connection::SpgConnection;
25
26/// Options for opening an [`SpgConnection`].
27///
28/// v7.16.0 — every clone of an `SpgConnectOptions` shares the
29/// same underlying `AsyncDatabase` once the first `connect()`
30/// resolves. That's the key to making `sqlx::Pool<Spg>` behave
31/// the way mailrs expects: `pool.begin()` and a separate
32/// `pool.acquire()` on the same pool reach the same in-process
33/// engine, so transaction visibility works.
34#[derive(Debug, Clone)]
35pub struct SpgConnectOptions {
36    /// Where to open the database. `None` → in-memory.
37    pub path: Option<PathBuf>,
38    /// sqlx log settings — adapter-level no-op for v7.16.0 but
39    /// preserved so the `log_statements` / `log_slow_statements`
40    /// builders compile.
41    pub log_settings: LogSettings,
42    /// Lazily-initialised shared engine. Constructed on the
43    /// first `connect()` call; every subsequent `connect()` on
44    /// a clone of these options returns a fresh `SpgConnection`
45    /// that points at the same engine. Tokio's `OnceCell` keeps
46    /// concurrent initialisation safe.
47    pub(crate) shared: std::sync::Arc<tokio::sync::OnceCell<spg_embedded_tokio::AsyncDatabase>>,
48}
49
50impl Default for SpgConnectOptions {
51    fn default() -> Self {
52        Self {
53            path: None,
54            log_settings: LogSettings::default(),
55            shared: std::sync::Arc::new(tokio::sync::OnceCell::new()),
56        }
57    }
58}
59
60impl SpgConnectOptions {
61    /// Construct an in-memory database options handle.
62    #[must_use]
63    pub fn in_memory() -> Self {
64        Self::default()
65    }
66
67    /// Construct a file-backed options handle.
68    #[must_use]
69    pub fn file(path: impl Into<PathBuf>) -> Self {
70        Self {
71            path: Some(path.into()),
72            log_settings: LogSettings::default(),
73            shared: std::sync::Arc::new(tokio::sync::OnceCell::new()),
74        }
75    }
76
77    /// v7.37.5 (mailrs crash-recovery Ask 2) — force the shared
78    /// `OnceCell<AsyncDatabase>` back to empty so the next
79    /// `connect()` re-opens the catalog instead of handing back a
80    /// stale cached handle. Pairs with
81    /// `spg_embedded::Database::force_unlock`: when the operator
82    /// asserts "no one owns this catalog; nuke the lock", any
83    /// previously-shared `AsyncDatabase` that still holds the
84    /// engine in this process must also be discarded so the new
85    /// boot's `connect_with` opens fresh.
86    ///
87    /// Replaces the `Arc<OnceCell<_>>` with a brand-new empty cell;
88    /// existing clones of these options that already grabbed an
89    /// `Arc` clone to the prior cell keep using it (they're the
90    /// callers that opted in to the shared handle); this method
91    /// only resets THIS handle. The typical mailrs shape — one
92    /// `SpgConnectOptions` per pool, recycled at force_unlock
93    /// time — is correctly served.
94    pub fn clear_shared(&mut self) {
95        self.shared = std::sync::Arc::new(tokio::sync::OnceCell::new());
96    }
97}
98
99impl FromStr for SpgConnectOptions {
100    type Err = Error;
101
102    fn from_str(s: &str) -> Result<Self, Error> {
103        // Strip `spg:` / `spg://` prefix. Anything that remains
104        // is either `memory` (case-insensitive) or a file path.
105        let rest = s
106            .strip_prefix("spg://")
107            .or_else(|| s.strip_prefix("spg:"))
108            .unwrap_or(s);
109        if rest.is_empty() || rest.eq_ignore_ascii_case("memory") {
110            return Ok(Self::in_memory());
111        }
112        Ok(Self::file(rest))
113    }
114}
115
116impl ConnectOptions for SpgConnectOptions {
117    type Connection = SpgConnection;
118
119    fn from_url(url: &sqlx_core::Url) -> Result<Self, Error> {
120        // sqlx::Url drops the scheme; the path lives in
121        // `url.path()` (with a leading `/` for absolute paths).
122        // `host()` resolves to None for in-memory `spg:memory`.
123        if url.scheme() != "spg" {
124            return Err(Error::Configuration(
125                format!("expected spg:// scheme, got {:?}", url.scheme()).into(),
126            ));
127        }
128        let host = url.host_str().unwrap_or("");
129        let path = url.path();
130        let combined = match (host, path) {
131            ("", "") | ("", "/") => String::new(),
132            ("", p) => p.to_string(),
133            (h, "") | (h, "/") => h.to_string(),
134            (h, p) => format!("{h}{p}"),
135        };
136        SpgConnectOptions::from_str(&combined)
137    }
138
139    fn connect(&self) -> BoxFuture<'_, Result<SpgConnection, Error>> {
140        let path = self.path.clone();
141        let shared = std::sync::Arc::clone(&self.shared);
142        Box::pin(async move {
143            let inner = shared
144                .get_or_try_init(|| async {
145                    match path {
146                        None => Ok::<_, Error>(spg_embedded_tokio::AsyncDatabase::open_in_memory()),
147                        Some(p) => spg_embedded_tokio::AsyncDatabase::open_path(p)
148                            .await
149                            .map_err(crate::error::engine_to_sqlx),
150                    }
151                })
152                .await?
153                .clone();
154            Ok(SpgConnection::new(inner))
155        })
156    }
157
158    fn log_statements(mut self, level: LevelFilter) -> Self {
159        self.log_settings.log_statements(level);
160        self
161    }
162
163    fn log_slow_statements(mut self, level: LevelFilter, duration: Duration) -> Self {
164        self.log_settings.log_slow_statements(level, duration);
165        self
166    }
167}