Skip to main content

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, false)?;
130            Ok(Arc::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    /// If `false` (the default), disallows `ATTACH`ing an existing file to a
145    /// database connection.
146    ///
147    /// Note: Attaching a new tempfile or `:memory:` database is always allowed.
148    #[serde(default)]
149    pub allow_attach_file: bool,
150}
151
152impl InProcDatabase {
153    /// Get a new connection creator for a local database.
154    ///
155    /// `base_dir` is the base directory path from which `path` is resolved if it is a relative path.
156    fn connection_creator(
157        self,
158        base_dir: &Path,
159    ) -> anyhow::Result<impl ConnectionCreator + 'static> {
160        let path = self
161            .path
162            .as_ref()
163            .map(|p| resolve_relative_path(p, base_dir));
164        let location = InProcDatabaseLocation::from_path(path)?;
165        let factory = move || {
166            let connection = spin_sqlite_inproc::InProcConnection::new(
167                location.clone(),
168                self.allow_attach_file,
169            )?;
170            Ok(Arc::new(connection) as _)
171        };
172        Ok(factory)
173    }
174}
175
176/// Resolve a relative path against a base dir.
177///
178/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
179fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
180    if path.is_absolute() {
181        return path.to_owned();
182    }
183    base_dir.join(path)
184}
185
186/// Configuration for a libSQL database.
187///
188/// This is used to deserialize the specific runtime config toml for libSQL databases.
189#[derive(Clone, Debug, Deserialize)]
190#[serde(deny_unknown_fields)]
191pub struct LibSqlDatabase {
192    url: String,
193    token: String,
194}
195
196impl LibSqlDatabase {
197    /// Get a new connection creator for a libSQL database.
198    fn connection_creator(self) -> anyhow::Result<impl ConnectionCreator> {
199        let url = check_url(&self.url)
200            .with_context(|| {
201                format!(
202                    "unexpected libSQL URL '{}' in runtime config file ",
203                    self.url
204                )
205            })?
206            .to_owned();
207        let factory = move || {
208            let connection = LazyLibSqlConnection::new(url.clone(), self.token.clone());
209            Ok(Arc::new(connection) as _)
210        };
211        Ok(factory)
212    }
213}
214
215// Checks an incoming url is in the shape we expect
216fn check_url(url: &str) -> anyhow::Result<&str> {
217    if url.starts_with("https://") || url.starts_with("http://") {
218        Ok(url)
219    } else {
220        Err(anyhow::anyhow!(
221            "URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)"
222        ))
223    }
224}