Skip to main content

vs_store/store/
mod.rs

1//! The [`Store`]: a SQLite-backed handle that owns a single connection.
2//!
3//! Per-table CRUD lives in submodules ([`sessions`], [`pages`],
4//! [`refs`], [`marks`], [`annotations`], [`actions`], [`auth_blobs`],
5//! [`skill_cache`]) — each contributes one `impl Store` block.
6
7mod actions;
8mod annotations;
9mod auth_blobs;
10mod marks;
11mod pages;
12mod refs;
13mod sessions;
14mod skill_cache;
15
16#[cfg(test)]
17mod tests;
18
19use std::path::Path;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22use rusqlite::{Connection, OpenFlags};
23
24use crate::error::Result;
25use crate::migrate;
26
27/// The default idempotency window: a `vs_act` repeat is treated as a
28/// hit only if the prior call started within this many seconds.
29pub const IDEMPOTENCY_TTL_SECS: i64 = 30;
30
31/// SQLite-backed durable state for vibesurfer.
32pub struct Store {
33    conn: Connection,
34}
35
36impl Store {
37    /// Open the store at `path`, applying any not-yet-applied
38    /// migrations and configuring WAL mode and foreign keys.
39    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
40        let conn = Connection::open(path.as_ref())?;
41        configure(&conn)?;
42        migrate::apply(&conn)?;
43        Ok(Self { conn })
44    }
45
46    /// Open an in-memory store (tests only — dropped when the handle
47    /// goes out of scope).
48    pub fn open_in_memory() -> Result<Self> {
49        let conn = Connection::open_in_memory()?;
50        configure(&conn)?;
51        migrate::apply(&conn)?;
52        Ok(Self { conn })
53    }
54
55    /// Open `path` read-only, *without* running migrations. Use for
56    /// `vs_log` queries that should never block on a write lock.
57    pub fn open_read_only(path: impl AsRef<Path>) -> Result<Self> {
58        let conn = Connection::open_with_flags(
59            path.as_ref(),
60            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
61        )?;
62        Ok(Self { conn })
63    }
64
65    /// Borrow the underlying connection. Per-table submodules use this
66    /// instead of `self.conn` to keep the field private.
67    #[must_use]
68    pub fn conn(&self) -> &Connection {
69        &self.conn
70    }
71}
72
73fn configure(conn: &Connection) -> Result<()> {
74    conn.pragma_update(None, "journal_mode", "WAL")?;
75    conn.pragma_update(None, "synchronous", "NORMAL")?;
76    conn.pragma_update(None, "foreign_keys", "ON")?;
77    Ok(())
78}
79
80/// Current Unix time in seconds, saturating at `i64::MAX` for the
81/// year-292277026596 case.
82#[must_use]
83pub fn epoch_secs() -> i64 {
84    SystemTime::now()
85        .duration_since(UNIX_EPOCH)
86        .map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
87}