1use std::path::{Path, PathBuf};
29use std::time::Duration;
30
31use rusqlite::Connection;
32
33use crate::paths::Paths;
34
35use super::error::StateError;
36use super::remote::{RemoteDb, from_libsql, rusqlite_shim, to_libsql_params};
37use super::row::Row;
38use super::value::Value;
39
40const MIGRATIONS: &[&str] = &[
41 include_str!("migrations/001_init.sql"),
42 include_str!("migrations/002_definition_dir.sql"),
43 include_str!("migrations/003_reaper.sql"),
44 include_str!("migrations/004_lock_host.sql"),
45 include_str!("migrations/005_dirty.sql"),
46];
47
48const MIGRATION_001_TABLES: &[&str] = &["instances", "leases", "op_locks", "checkpoints"];
49
50const REMOTE_SCHEMA_VERSION_BOOTSTRAP: &str = r"
53CREATE TABLE IF NOT EXISTS _stackless_schema_version (
54 version INTEGER NOT NULL
55);
56";
57
58enum Backend {
59 Local(Connection),
60 Remote(RemoteDb),
61}
62
63pub struct Store {
64 backend: Backend,
65}
66
67impl std::fmt::Debug for Store {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("Store").finish_non_exhaustive()
70 }
71}
72
73impl Store {
74 pub fn open(path: &Path) -> Result<Self, StateError> {
76 if let Some(dir) = path.parent() {
77 std::fs::create_dir_all(dir).map_err(|source| StateError::StateDir {
78 path: dir.display().to_string(),
79 source,
80 })?;
81 }
82 let conn = Connection::open(path).map_err(|source| StateError::Open {
83 path: path.display().to_string(),
84 source,
85 })?;
86 conn.busy_timeout(Duration::from_secs(5))?;
87 conn.pragma_update(None, "journal_mode", "wal")?;
88 conn.pragma_update(None, "foreign_keys", "on")?;
89 let store = Self {
90 backend: Backend::Local(conn),
91 };
92 store.migrate()?;
93 Ok(store)
94 }
95
96 pub fn open_remote(url: &str, token: &str) -> Result<Self, StateError> {
98 let store = Self {
99 backend: Backend::Remote(RemoteDb::open_remote(url.to_owned(), token.to_owned())?),
100 };
101 store.migrate()?;
102 Ok(store)
103 }
104
105 #[doc(hidden)]
109 pub fn open_libsql_local(path: &str) -> Result<Self, StateError> {
110 let store = Self {
111 backend: Backend::Remote(RemoteDb::open_local(path.to_owned())?),
112 };
113 store.migrate()?;
114 Ok(store)
115 }
116
117 pub fn open_configured() -> Result<Self, StateError> {
120 Self::open_with_paths(&Paths::from_env())
121 }
122
123 pub fn open_with_paths(paths: &Paths) -> Result<Self, StateError> {
126 match std::env::var("STACKLESS_STATE_URL") {
127 Ok(url) if !url.is_empty() => {
128 let token = std::env::var("STACKLESS_STATE_TOKEN").unwrap_or_default();
129 Self::open_remote(&url, &token)
130 }
131 _ => Self::open(&paths.db_path()),
132 }
133 }
134
135 pub fn state_dir() -> PathBuf {
137 Paths::from_env().state_dir().to_path_buf()
138 }
139
140 pub fn default_path() -> PathBuf {
143 Paths::from_env().db_path()
144 }
145
146 fn migrate(&self) -> Result<(), StateError> {
147 if let Backend::Remote(db) = &self.backend {
148 self.repair_remote_migration_001()?;
149 self.reconcile_remote_schema_version(db)?;
150 }
151 let version = self.user_version()?;
152 for (index, sql) in MIGRATIONS.iter().enumerate() {
153 let target = index as i64 + 1;
154 if version >= target {
155 continue;
156 }
157 self.run_migration(sql, target)?;
158 }
159 Ok(())
160 }
161
162 fn repair_remote_migration_001(&self) -> Result<(), StateError> {
164 let Backend::Remote(db) = &self.backend else {
165 return Ok(());
166 };
167 if self.migration_001_complete()? || !self.migration_001_started()? {
168 return Ok(());
169 }
170 db.run(|conn, rt| {
171 rt.block_on(async {
172 conn.execute_batch(include_str!("migrations/001_init_repair.sql"))
173 .await
174 .map_err(|e| StateError::Migrate {
175 source: rusqlite_shim(e),
176 })?;
177 Ok(())
178 })
179 })
180 }
181
182 fn migration_001_complete(&self) -> Result<bool, StateError> {
183 for table in MIGRATION_001_TABLES {
184 if !self.table_exists(table)? {
185 return Ok(false);
186 }
187 }
188 Ok(true)
189 }
190
191 fn migration_001_started(&self) -> Result<bool, StateError> {
192 for table in MIGRATION_001_TABLES {
193 if self.table_exists(table)? {
194 return Ok(true);
195 }
196 }
197 Ok(false)
198 }
199
200 fn reconcile_remote_schema_version(&self, db: &RemoteDb) -> Result<(), StateError> {
204 let recorded = Self::remote_user_version(db)?;
205 let actual = self.detect_migration_level()?;
206 if actual > recorded {
207 Self::remote_set_user_version(db, actual)?;
208 }
209 Ok(())
210 }
211
212 fn detect_migration_level(&self) -> Result<i64, StateError> {
213 let mut level = 0;
214 if self.migration_001_complete()? {
215 level = 1;
216 if self.column_exists("instances", "definition_dir")? {
217 level = 2;
218 }
219 if self.table_exists("reap_attempts")? {
220 level = 3;
221 }
222 if self.column_exists("op_locks", "holder_host")? {
223 level = 4;
224 }
225 if self.column_exists("instances", "dirty")? {
226 level = 5;
227 }
228 }
229 Ok(level)
230 }
231
232 fn table_exists(&self, name: &str) -> Result<bool, StateError> {
233 Ok(self
234 .query_first(
235 "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1",
236 &[name.into()],
237 )?
238 .is_some())
239 }
240
241 fn column_exists(&self, table: &str, column: &str) -> Result<bool, StateError> {
242 Ok(self
243 .query_first(
244 &format!("SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1"),
245 &[column.into()],
246 )?
247 .is_some())
248 }
249
250 fn user_version(&self) -> Result<i64, StateError> {
251 match &self.backend {
252 Backend::Local(conn) => conn
253 .pragma_query_value(None, "user_version", |row| row.get(0))
254 .map_err(Into::into),
255 Backend::Remote(db) => Self::remote_user_version(db),
256 }
257 }
258
259 fn ensure_remote_schema_version_table(db: &RemoteDb) -> Result<(), StateError> {
260 db.run(|conn, rt| {
261 rt.block_on(async {
262 conn.execute_batch(REMOTE_SCHEMA_VERSION_BOOTSTRAP)
263 .await
264 .map_err(|e| StateError::Migrate {
265 source: rusqlite_shim(e),
266 })?;
267 conn.execute(
268 "INSERT INTO _stackless_schema_version (version)
269 SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM _stackless_schema_version)",
270 (),
271 )
272 .await
273 .map_err(|e| StateError::Migrate {
274 source: rusqlite_shim(e),
275 })?;
276 Ok(())
277 })
278 })
279 }
280
281 fn remote_user_version(db: &RemoteDb) -> Result<i64, StateError> {
282 Self::ensure_remote_schema_version_table(db)?;
283 db.run(|conn, rt| {
284 rt.block_on(async {
285 let mut rows = conn
286 .query(
287 "SELECT COALESCE(MAX(version), 0) FROM _stackless_schema_version",
288 (),
289 )
290 .await
291 .map_err(StateError::remote_query)?;
292 match rows.next().await.map_err(StateError::remote_query)? {
293 Some(row) => row.get::<i64>(0).map_err(StateError::remote_query),
294 None => Ok(0),
295 }
296 })
297 })
298 }
299
300 fn remote_set_user_version(db: &RemoteDb, target: i64) -> Result<(), StateError> {
301 Self::ensure_remote_schema_version_table(db)?;
302 db.run(move |conn, rt| {
303 rt.block_on(async {
304 let changed = conn
305 .execute(
306 "UPDATE _stackless_schema_version SET version = ?1",
307 [libsql::Value::Integer(target)],
308 )
309 .await
310 .map_err(|e| StateError::Migrate {
311 source: rusqlite_shim(e),
312 })?;
313 if changed == 0 {
314 let mut rows = conn
317 .query("SELECT 1 FROM _stackless_schema_version LIMIT 1", ())
318 .await
319 .map_err(|e| StateError::Migrate {
320 source: rusqlite_shim(e),
321 })?;
322 if rows
323 .next()
324 .await
325 .map_err(|e| StateError::Migrate {
326 source: rusqlite_shim(e),
327 })?
328 .is_none()
329 {
330 conn.execute(
331 "INSERT INTO _stackless_schema_version (version) VALUES (?1)",
332 [libsql::Value::Integer(target)],
333 )
334 .await
335 .map_err(|e| StateError::Migrate {
336 source: rusqlite_shim(e),
337 })?;
338 }
339 }
340 Ok(())
341 })
342 })
343 }
344
345 fn run_migration(&self, sql: &str, target: i64) -> Result<(), StateError> {
346 match &self.backend {
347 Backend::Local(conn) => conn
348 .execute_batch(&format!(
349 "BEGIN; {sql} ; PRAGMA user_version = {target}; COMMIT;"
350 ))
351 .map_err(|source| StateError::Migrate { source }),
352 Backend::Remote(db) => {
353 let sql = sql.to_owned();
358 db.run(move |conn, rt| {
359 rt.block_on(async {
360 conn.execute_batch(&sql)
361 .await
362 .map_err(|e| StateError::Migrate {
363 source: rusqlite_shim(e),
364 })?;
365 Ok(())
366 })
367 })?;
368 Self::remote_set_user_version(db, target)
369 }
370 }
371 }
372
373 pub(super) fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, StateError> {
377 match &self.backend {
378 Backend::Local(conn) => conn
379 .execute(
380 sql,
381 rusqlite::params_from_iter(params.iter().map(to_rusqlite)),
382 )
383 .map(|n| n as u64)
384 .map_err(Into::into),
385 Backend::Remote(db) => {
386 let sql = sql.to_owned();
387 let params = to_libsql_params(params);
388 db.run(move |conn, rt| {
389 rt.block_on(async {
390 conn.execute(&sql, params)
391 .await
392 .map_err(StateError::remote_query)
393 })
394 })
395 }
396 }
397 }
398
399 pub(super) fn query_row<T, F>(
403 &self,
404 sql: &str,
405 params: &[Value],
406 map: F,
407 ) -> Result<Option<T>, StateError>
408 where
409 F: FnOnce(&Row) -> Result<T, StateError>,
410 {
411 match self.query_first(sql, params)? {
412 Some(row) => map(&row).map(Some),
413 None => Ok(None),
414 }
415 }
416
417 pub(super) fn query_map<T, F>(
419 &self,
420 sql: &str,
421 params: &[Value],
422 map: F,
423 ) -> Result<Vec<T>, StateError>
424 where
425 F: FnMut(&Row) -> Result<T, StateError>,
426 {
427 let rows = self.collect_rows(sql, params, usize::MAX)?;
428 rows.iter().map(map).collect()
429 }
430
431 fn query_first(&self, sql: &str, params: &[Value]) -> Result<Option<Row>, StateError> {
433 Ok(self.collect_rows(sql, params, 1)?.into_iter().next())
434 }
435
436 fn collect_rows(
438 &self,
439 sql: &str,
440 params: &[Value],
441 limit: usize,
442 ) -> Result<Vec<Row>, StateError> {
443 match &self.backend {
444 Backend::Local(conn) => {
445 let mut stmt = conn.prepare(sql)?;
446 let col_count = stmt.column_count();
447 let mut out = Vec::new();
448 let mut rows =
449 stmt.query(rusqlite::params_from_iter(params.iter().map(to_rusqlite)))?;
450 while let Some(row) = rows.next()? {
451 let mut columns = Vec::with_capacity(col_count);
452 for i in 0..col_count {
453 columns.push(from_rusqlite(row, i)?);
454 }
455 out.push(Row::from_columns(columns));
456 if out.len() >= limit {
457 break;
458 }
459 }
460 Ok(out)
461 }
462 Backend::Remote(db) => {
463 let sql = sql.to_owned();
464 let params = to_libsql_params(params);
465 db.run(move |conn, rt| {
466 rt.block_on(async {
467 let mut rows = conn
468 .query(&sql, params)
469 .await
470 .map_err(StateError::remote_query)?;
471 let column_count = rows.column_count();
472 let mut out = Vec::new();
473 while let Some(row) = rows.next().await.map_err(StateError::remote_query)? {
474 out.push(from_libsql(&row, column_count)?);
475 if out.len() >= limit {
476 break;
477 }
478 }
479 Ok(out)
480 })
481 })
482 }
483 }
484 }
485
486 #[doc(hidden)]
489 pub fn conn_for_tests(&self) -> &Connection {
490 match &self.backend {
491 Backend::Local(conn) => conn,
492 Backend::Remote(_) => unreachable!("conn_for_tests is local-only"),
493 }
494 }
495
496 #[doc(hidden)]
500 pub fn execute_for_tests(&self, sql: &str, params: &[&str]) -> Result<u64, StateError> {
501 let owned: Vec<Value> = params
502 .iter()
503 .map(|s| Value::Text((*s).to_owned()))
504 .collect();
505 self.execute(sql, &owned)
506 }
507
508 pub(super) fn hostname() -> String {
511 sysinfo::System::host_name().unwrap_or_default()
512 }
513
514 pub(super) fn now() -> i64 {
516 std::time::SystemTime::now()
517 .duration_since(std::time::UNIX_EPOCH)
518 .map(|d| d.as_secs() as i64)
519 .unwrap_or(0)
520 }
521
522 pub fn now_secs() -> i64 {
525 Self::now()
526 }
527}
528
529fn to_rusqlite(v: &Value) -> Box<dyn rusqlite::types::ToSql> {
532 match v {
533 Value::Text(s) => Box::new(s.clone()),
534 Value::Int(i) => Box::new(*i),
535 Value::Null => Box::new(rusqlite::types::Null),
536 }
537}
538
539fn from_rusqlite(row: &rusqlite::Row<'_>, idx: usize) -> Result<Value, StateError> {
540 use rusqlite::types::ValueRef;
541 Ok(match row.get_ref(idx)? {
542 ValueRef::Null => Value::Null,
543 ValueRef::Integer(i) => Value::Int(i),
544 ValueRef::Text(t) => Value::Text(String::from_utf8_lossy(t).into_owned()),
545 ValueRef::Real(_) => {
546 return Err(StateError::row_type(idx, "int|text|null (got real)"));
547 }
548 ValueRef::Blob(_) => {
549 return Err(StateError::row_type(idx, "int|text|null (got blob)"));
550 }
551 })
552}