Skip to main content

valence_backend_sqlite/
backend.rs

1//! SQLite storage engine.
2
3use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
4use std::str::FromStr;
5
6use valence_backend_sql::{
7    create_record_sqlite, define_unique_index_sqlite, delete_record_sqlite, ensure_table_sqlite,
8    execute_select_sqlite, get_edge_targets_sqlite, get_record_sqlite, merge_record_sqlite,
9    relate_edge_sqlite, sql_capabilities, ttl_deferred, unrelate_edge_sqlite, update_record_sqlite,
10};
11use valence_core::backend::DatabaseBackend;
12use valence_core::compiled_query::CompiledQuery;
13use valence_core::error::{Error, Result};
14use valence_core::record_id::RecordId;
15use valence_core::ttl::SchemaTtlPolicy;
16use valence_core::{Database, DatabaseFromEngine, KnownEngines};
17
18/// Stable engine slug for router keys (`sqlite:logical_name`).
19pub const ENGINE_ID: &str = KnownEngines::SQLITE;
20
21/// Schema evaluator const for `database:` routing.
22pub const PRIMARY: DatabaseFromEngine = Database::from_engine("primary", ENGINE_ID);
23
24/// SQLite-backed [`DatabaseBackend`] using JSON document rows.
25///
26/// # Examples
27///
28/// ```ignore
29/// use std::sync::Arc;
30/// use valence::{
31///     valence_schema, Database, DatabaseFromEngine, FieldType, SqliteBackend, Valence,
32///     SQLITE_ENGINE_ID,
33/// };
34///
35/// const COUNTER_DB: DatabaseFromEngine =
36///     Database::from_engine("default", SQLITE_ENGINE_ID);
37///
38/// valence_schema! {
39///     Counter {
40///         table: "counter",
41///         version: "0.1.0",
42///         database: COUNTER_DB,
43///         fields: [
44///             id: { r#type: FieldType::String, primary_key: true, required: true },
45///             value: { r#type: FieldType::Integer, required: true },
46///         ],
47///     }
48/// }
49///
50/// let backend = SqliteBackend::connect_memory().await?;
51/// let valence = Valence::builder()
52///     .add_backend("default", Arc::new(backend))
53///     .build()?;
54/// assert_eq!(
55///     valence.backend_for_table("counter")?.engine_id(),
56///     SQLITE_ENGINE_ID
57/// );
58/// # Ok::<(), valence::Error>(())
59/// ```
60#[derive(Debug, Clone)]
61pub struct SqliteBackend {
62    pool: SqlitePool,
63}
64
65impl SqliteBackend {
66    /// Connect to an in-memory SQLite database.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the in-memory connection or edges schema setup fails.
71    pub async fn connect_memory() -> Result<Self> {
72        Self::connect(":memory:").await
73    }
74
75    /// Connect to a SQLite database at `path` (`:memory:` for ephemeral).
76    ///
77    /// In-memory URLs (`:memory:` or `mode=memory`) use a single-connection pool so every
78    /// checkout sees the same store. sqlx's default pool size otherwise opens isolated
79    /// private databases per connection for bare `:memory:`.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`Error::Database`] if connecting or ensuring the edges schema fails.
84    pub async fn connect(path: &str) -> Result<Self> {
85        let options = SqliteConnectOptions::from_str(path)
86            .or_else(|_| SqliteConnectOptions::from_str(&format!("sqlite:{path}")))
87            .map_err(|e| Error::database(e.to_string()))?
88            .create_if_missing(true);
89        let memory =
90            path.contains(":memory:") || path.contains("mode=memory") || path == ":memory:";
91        let mut pool_opts = SqlitePoolOptions::new();
92        if memory {
93            pool_opts = pool_opts.max_connections(1);
94        }
95        let pool = pool_opts
96            .connect_with(options)
97            .await
98            .map_err(|e| Error::database(e.to_string()))?;
99        valence_backend_sql::ensure_edges_sqlite(&pool).await?;
100        Ok(Self { pool })
101    }
102
103    /// Borrow the underlying pool.
104    pub fn pool(&self) -> &SqlitePool {
105        &self.pool
106    }
107}
108
109#[async_trait::async_trait]
110impl DatabaseBackend for SqliteBackend {
111    fn engine_id(&self) -> &'static str {
112        ENGINE_ID
113    }
114
115    fn capabilities(&self) -> valence_core::BackendCapabilities {
116        sql_capabilities("sqlite")
117    }
118
119    async fn execute_compiled_query(
120        &self,
121        compiled: &CompiledQuery,
122    ) -> Result<Vec<serde_json::Value>> {
123        execute_select_sqlite(&self.pool, compiled, "").await
124    }
125
126    async fn ensure_schemaless_table(&self, table: &str) -> Result<()> {
127        ensure_table_sqlite(&self.pool, table).await
128    }
129
130    async fn get_record(&self, table: &str, id: &str) -> Result<Option<serde_json::Value>> {
131        get_record_sqlite(&self.pool, table, id).await
132    }
133
134    async fn create_record(
135        &self,
136        table: &str,
137        content: serde_json::Value,
138    ) -> Result<serde_json::Value> {
139        create_record_sqlite(&self.pool, table, content).await
140    }
141
142    async fn update_record(
143        &self,
144        table: &str,
145        id: &str,
146        content: serde_json::Value,
147    ) -> Result<serde_json::Value> {
148        update_record_sqlite(&self.pool, table, id, content).await
149    }
150
151    async fn merge_record(
152        &self,
153        table: &str,
154        id: &str,
155        patch: serde_json::Value,
156    ) -> Result<serde_json::Value> {
157        merge_record_sqlite(&self.pool, table, id, patch).await
158    }
159
160    async fn upsert_record(
161        &self,
162        table: &str,
163        id: &str,
164        content: serde_json::Value,
165    ) -> Result<serde_json::Value> {
166        if self.get_record(table, id).await?.is_some() {
167            self.update_record(table, id, content).await
168        } else {
169            let mut c = content;
170            if let Some(obj) = c.as_object_mut() {
171                obj.insert("id".into(), serde_json::json!({"table": table, "id": id}));
172            }
173            self.create_record(table, c).await
174        }
175    }
176
177    async fn delete_record(&self, table: &str, id: &str) -> Result<()> {
178        delete_record_sqlite(&self.pool, table, id).await
179    }
180
181    async fn relate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
182        relate_edge_sqlite(&self.pool, from, edge_table, to).await
183    }
184
185    async fn unrelate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()> {
186        unrelate_edge_sqlite(&self.pool, from, edge_table, to).await
187    }
188
189    async fn get_edge_targets(&self, from: &RecordId, edge_table: &str) -> Result<Vec<RecordId>> {
190        get_edge_targets_sqlite(&self.pool, from, edge_table).await
191    }
192
193    async fn define_unique_index(&self, table: &str, field: &str) -> Result<()> {
194        define_unique_index_sqlite(&self.pool, table, field).await
195    }
196
197    fn ttl_capability(&self) -> valence_core::ttl::BackendTtlCapability {
198        ttl_deferred()
199    }
200
201    async fn apply_ttl_policy(&self, _table: &str, _policy: &SchemaTtlPolicy) -> Result<()> {
202        Ok(())
203    }
204}