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}