spin_sqlite/
lib.rs

1//! Spin's default handling of the runtime configuration for SQLite databases.
2
3use std::{
4    collections::HashMap,
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use serde::Deserialize;
10use spin_factor_sqlite::ConnectionCreator;
11use spin_factors::{
12    anyhow::{self, Context as _},
13    runtime_config::toml::GetTomlValue,
14};
15use spin_sqlite_inproc::InProcDatabaseLocation;
16use spin_sqlite_libsql::LazyLibSqlConnection;
17
18/// Spin's default resolution of runtime configuration for SQLite databases.
19///
20/// This type implements how Spin CLI's SQLite implementation is configured
21/// through the runtime config toml as well as the behavior of the "default" label.
22#[derive(Clone, Debug)]
23pub struct RuntimeConfigResolver {
24    default_database_dir: Option<PathBuf>,
25    local_database_dir: PathBuf,
26}
27
28impl RuntimeConfigResolver {
29    /// Create a new `SpinSqliteRuntimeConfig`
30    ///
31    /// This takes as arguments:
32    /// * the directory to use as the default location for SQLite databases.
33    ///   Usually this will be the path to the `.spin` state directory. If
34    ///   `None`, the default database will be in-memory.
35    /// * the path to the directory from which relative paths to
36    ///   local SQLite databases are resolved.  (this should most likely be the
37    ///   path to the runtime-config file or the current working dir).
38    pub fn new(default_database_dir: Option<PathBuf>, local_database_dir: PathBuf) -> Self {
39        Self {
40            default_database_dir,
41            local_database_dir,
42        }
43    }
44
45    /// Get the runtime configuration for SQLite databases from a TOML table.
46    ///
47    /// Expects table to be in the format:
48    /// ````toml
49    /// [sqlite_database.$database-label]
50    /// type = "$database-type"
51    /// ... extra type specific configuration ...
52    /// ```
53    ///
54    /// Configuration is automatically added for the 'default' label if it is not provided.
55    pub fn resolve(
56        &self,
57        table: &impl GetTomlValue,
58    ) -> anyhow::Result<spin_factor_sqlite::runtime_config::RuntimeConfig> {
59        let mut runtime_config = self.resolve_from_toml(table)?.unwrap_or_default();
60        // If the user did not provide configuration for the default label, add it.
61        if !runtime_config.connection_creators.contains_key("default") {
62            runtime_config
63                .connection_creators
64                .insert("default".to_owned(), self.default());
65        }
66
67        Ok(runtime_config)
68    }
69
70    /// Get the runtime configuration for SQLite databases from a TOML table.
71    fn resolve_from_toml(
72        &self,
73        table: &impl GetTomlValue,
74    ) -> anyhow::Result<Option<spin_factor_sqlite::runtime_config::RuntimeConfig>> {
75        let Some(table) = table.get("sqlite_database") else {
76            return Ok(None);
77        };
78        let config: std::collections::HashMap<String, TomlRuntimeConfig> =
79            table.clone().try_into()?;
80        let connection_creators = config
81            .into_iter()
82            .map(|(k, v)| Ok((k, self.get_connection_creator(v)?)))
83            .collect::<anyhow::Result<HashMap<_, _>>>()?;
84
85        Ok(Some(spin_factor_sqlite::runtime_config::RuntimeConfig {
86            connection_creators,
87        }))
88    }
89
90    /// Get a connection creator for a given runtime configuration.
91    pub fn get_connection_creator(
92        &self,
93        config: TomlRuntimeConfig,
94    ) -> anyhow::Result<Arc<dyn ConnectionCreator>> {
95        let database_kind = config.type_.as_str();
96        match database_kind {
97            "spin" => {
98                let config: InProcDatabase = config.config.try_into()?;
99                Ok(Arc::new(
100                    config.connection_creator(&self.local_database_dir)?,
101                ))
102            }
103            "libsql" => {
104                let config: LibSqlDatabase = config.config.try_into()?;
105                Ok(Arc::new(config.connection_creator()?))
106            }
107            _ => anyhow::bail!("Unknown database kind: {database_kind}"),
108        }
109    }
110}
111
112#[derive(Deserialize)]
113pub struct TomlRuntimeConfig {
114    #[serde(rename = "type")]
115    pub type_: String,
116    #[serde(flatten)]
117    pub config: toml::Table,
118}
119
120impl RuntimeConfigResolver {
121    /// The [`ConnectionCreator`] for the 'default' label.
122    pub fn default(&self) -> Arc<dyn ConnectionCreator> {
123        let path = self
124            .default_database_dir
125            .as_deref()
126            .map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME));
127        let factory = move || {
128            let location = InProcDatabaseLocation::from_path(path.clone())?;
129            let connection = spin_sqlite_inproc::InProcConnection::new(location)?;
130            Ok(Box::new(connection) as _)
131        };
132        Arc::new(factory)
133    }
134}
135
136const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db";
137
138/// Configuration for a local SQLite database.
139#[derive(Clone, Debug, Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct InProcDatabase {
142    pub path: Option<PathBuf>,
143}
144
145impl InProcDatabase {
146    /// Get a new connection creator for a local database.
147    ///
148    /// `base_dir` is the base directory path from which `path` is resolved if it is a relative path.
149    fn connection_creator(self, base_dir: &Path) -> anyhow::Result<impl ConnectionCreator> {
150        let path = self
151            .path
152            .as_ref()
153            .map(|p| resolve_relative_path(p, base_dir));
154        let location = InProcDatabaseLocation::from_path(path)?;
155        let factory = move || {
156            let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?;
157            Ok(Box::new(connection) as _)
158        };
159        Ok(factory)
160    }
161}
162
163/// Resolve a relative path against a base dir.
164///
165/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
166fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
167    if path.is_absolute() {
168        return path.to_owned();
169    }
170    base_dir.join(path)
171}
172
173/// Configuration for a libSQL database.
174///
175/// This is used to deserialize the specific runtime config toml for libSQL databases.
176#[derive(Clone, Debug, Deserialize)]
177#[serde(deny_unknown_fields)]
178pub struct LibSqlDatabase {
179    url: String,
180    token: String,
181}
182
183impl LibSqlDatabase {
184    /// Get a new connection creator for a libSQL database.
185    fn connection_creator(self) -> anyhow::Result<impl ConnectionCreator> {
186        let url = check_url(&self.url)
187            .with_context(|| {
188                format!(
189                    "unexpected libSQL URL '{}' in runtime config file ",
190                    self.url
191                )
192            })?
193            .to_owned();
194        let factory = move || {
195            let connection = LazyLibSqlConnection::new(url.clone(), self.token.clone());
196            Ok(Box::new(connection) as _)
197        };
198        Ok(factory)
199    }
200}
201
202// Checks an incoming url is in the shape we expect
203fn check_url(url: &str) -> anyhow::Result<&str> {
204    if url.starts_with("https://") || url.starts_with("http://") {
205        Ok(url)
206    } else {
207        Err(anyhow::anyhow!(
208            "URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)"
209        ))
210    }
211}