From d59305bc5bbd911c68da0a0e7afd182ddfc5f1e9 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Wed, 28 Aug 2024 16:11:33 +0200 Subject: [PATCH 1/3] Remove old sqlite code Signed-off-by: Ryan Levick --- crates/sqlite/src/host_component.rs | 101 --------------- crates/sqlite/src/lib.rs | 183 +--------------------------- 2 files changed, 1 insertion(+), 283 deletions(-) delete mode 100644 crates/sqlite/src/host_component.rs diff --git a/crates/sqlite/src/host_component.rs b/crates/sqlite/src/host_component.rs deleted file mode 100644 index 1a48b2ee8..000000000 --- a/crates/sqlite/src/host_component.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::sync::Arc; - -use crate::{ConnectionsStore, SqliteDispatch, DATABASES_KEY}; -use anyhow::anyhow; -use spin_app::{AppComponent, DynamicHostComponent}; -use spin_core::HostComponent; -use spin_world::v2::sqlite; - -type InitConnectionsStore = dyn (Fn(&AppComponent) -> Arc) + Sync + Send; - -pub struct SqliteComponent { - /// Function that can be called when a `ConnectionsStore` is needed - init_connections_store: Box, -} - -impl SqliteComponent { - pub fn new(init_connections_store: F) -> Self - where - F: (Fn(&AppComponent) -> Arc) + Sync + Send + 'static, - { - Self { - init_connections_store: Box::new(init_connections_store), - } - } -} - -impl HostComponent for SqliteComponent { - type Data = super::SqliteDispatch; - - fn add_to_linker( - linker: &mut spin_core::Linker, - get: impl Fn(&mut spin_core::Data) -> &mut Self::Data + Send + Sync + Copy + 'static, - ) -> anyhow::Result<()> { - sqlite::add_to_linker(linker, get)?; - spin_world::v1::sqlite::add_to_linker(linker, get) - } - - fn build_data(&self) -> Self::Data { - // To initialize `SqliteDispatch` we need a `ConnectionsStore`, but we can't build one - // until we have a `ComponentApp`. That's fine though as we'll have one `DynamicHostComponent::update_data`. - // The Noop implementation will never get called. - struct Noop; - #[async_trait::async_trait] - impl ConnectionsStore for Noop { - async fn get_connection( - &self, - _database: &str, - ) -> Result>, sqlite::Error> { - debug_assert!(false, "`Noop` `ConnectionsStore` was called"); - Ok(None) - } - - fn has_connection_for(&self, _database: &str) -> bool { - debug_assert!(false, "`Noop` `ConnectionsStore` was called"); - false - } - } - SqliteDispatch::new(Arc::new(Noop)) - } -} - -impl DynamicHostComponent for SqliteComponent { - fn update_data(&self, data: &mut Self::Data, component: &AppComponent) -> anyhow::Result<()> { - let allowed_databases = component - .get_metadata(crate::DATABASES_KEY)? - .unwrap_or_default(); - data.component_init(allowed_databases, (self.init_connections_store)(component)); - Ok(()) - } - - fn validate_app(&self, app: &spin_app::App) -> anyhow::Result<()> { - let mut errors = vec![]; - - for component in app.components() { - let connections_store = (self.init_connections_store)(&component); - for allowed in component.get_metadata(DATABASES_KEY)?.unwrap_or_default() { - if !connections_store.has_connection_for(&allowed) { - let err = format!("- Component {} uses database '{allowed}'", component.id()); - errors.push(err); - } - } - } - - if errors.is_empty() { - Ok(()) - } else { - let prologue = vec![ - "One or more components use SQLite databases which are not defined.", - "Check the spelling, or pass a runtime configuration file that defines these stores.", - "See https://developer.fermyon.com/spin/dynamic-configuration#sqlite-storage-runtime-configuration", - "Details:", - ]; - let lines: Vec<_> = prologue - .into_iter() - .map(|s| s.to_owned()) - .chain(errors) - .collect(); - Err(anyhow!(lines.join("\n"))) - } - } -} diff --git a/crates/sqlite/src/lib.rs b/crates/sqlite/src/lib.rs index 110dfaca0..2ea4de250 100644 --- a/crates/sqlite/src/lib.rs +++ b/crates/sqlite/src/lib.rs @@ -1,26 +1,5 @@ -// TODO(factors): Code left for reference; remove after migration to factors -// mod host_component; - -use spin_app::MetadataKey; -use spin_core::wasmtime::component::Resource; -use spin_world::async_trait; -use spin_world::v1::sqlite::Error as V1SqliteError; +use spin_core::async_trait; use spin_world::v2::sqlite; -use std::{collections::HashSet, sync::Arc}; - -pub const DATABASES_KEY: MetadataKey> = MetadataKey::new("databases"); - -/// A store of connections for all accessible databases for an application -#[async_trait] -pub trait ConnectionsStore: Send + Sync { - /// Get a `Connection` for a specific database - async fn get_connection( - &self, - database: &str, - ) -> Result>, sqlite::Error>; - - fn has_connection_for(&self, database: &str) -> bool; -} /// A trait abstracting over operations to a SQLite database #[async_trait] @@ -33,163 +12,3 @@ pub trait Connection: Send + Sync { async fn execute_batch(&self, statements: &str) -> anyhow::Result<()>; } - -/// An implementation of the SQLite host -pub struct SqliteDispatch { - allowed_databases: HashSet, - connections: table::Table>, - connections_store: Arc, -} - -impl SqliteDispatch { - pub fn new(connections_store: Arc) -> Self { - Self { - connections: table::Table::new(256), - allowed_databases: HashSet::new(), - connections_store, - } - } - - /// (Re-)initialize dispatch for a give app - pub fn component_init( - &mut self, - allowed_databases: HashSet, - connections_store: Arc, - ) { - self.allowed_databases = allowed_databases; - self.connections_store = connections_store; - } - - fn get_connection( - &self, - connection: Resource, - ) -> Result<&Arc, sqlite::Error> { - self.connections - .get(connection.rep()) - .ok_or(sqlite::Error::InvalidConnection) - } -} - -#[async_trait] -impl sqlite::Host for SqliteDispatch { - fn convert_error(&mut self, error: sqlite::Error) -> anyhow::Result { - Ok(error) - } -} - -#[async_trait] -impl sqlite::HostConnection for SqliteDispatch { - async fn open( - &mut self, - database: String, - ) -> Result, sqlite::Error> { - if !self.allowed_databases.contains(&database) { - return Err(sqlite::Error::AccessDenied); - } - self.connections_store - .get_connection(&database) - .await - .and_then(|conn| conn.ok_or(sqlite::Error::NoSuchDatabase)) - .and_then(|conn| { - self.connections - .push(conn) - .map_err(|()| sqlite::Error::Io("too many connections opened".to_string())) - }) - .map(Resource::new_own) - } - - async fn execute( - &mut self, - connection: Resource, - query: String, - parameters: Vec, - ) -> Result { - let conn = match self.get_connection(connection) { - Ok(c) => c, - Err(err) => return Err(err), - }; - conn.query(&query, parameters).await - } - - fn drop(&mut self, connection: Resource) -> anyhow::Result<()> { - let _ = self.connections.remove(connection.rep()); - Ok(()) - } -} - -#[async_trait] -impl spin_world::v1::sqlite::Host for SqliteDispatch { - async fn open(&mut self, database: String) -> Result { - let result = ::open(self, database).await; - result.map_err(to_legacy_error).map(|s| s.rep()) - } - - async fn execute( - &mut self, - connection: u32, - query: String, - parameters: Vec, - ) -> Result { - let this = Resource::new_borrow(connection); - let result = ::execute( - self, - this, - query, - parameters.into_iter().map(from_legacy_value).collect(), - ) - .await; - result.map_err(to_legacy_error).map(to_legacy_query_result) - } - - async fn close(&mut self, connection: u32) -> anyhow::Result<()> { - ::drop(self, Resource::new_own(connection)) - } - - fn convert_error(&mut self, error: V1SqliteError) -> anyhow::Result { - Ok(error) - } -} -use spin_world::v1::sqlite as v1; - -fn to_legacy_error(error: sqlite::Error) -> v1::Error { - match error { - sqlite::Error::NoSuchDatabase => v1::Error::NoSuchDatabase, - sqlite::Error::AccessDenied => v1::Error::AccessDenied, - sqlite::Error::InvalidConnection => v1::Error::InvalidConnection, - sqlite::Error::DatabaseFull => v1::Error::DatabaseFull, - sqlite::Error::Io(s) => v1::Error::Io(s), - } -} - -fn to_legacy_query_result(result: sqlite::QueryResult) -> v1::QueryResult { - v1::QueryResult { - columns: result.columns, - rows: result.rows.into_iter().map(to_legacy_row_result).collect(), - } -} - -fn to_legacy_row_result(result: sqlite::RowResult) -> v1::RowResult { - v1::RowResult { - values: result.values.into_iter().map(to_legacy_value).collect(), - } -} - -fn to_legacy_value(value: sqlite::Value) -> v1::Value { - match value { - sqlite::Value::Integer(i) => v1::Value::Integer(i), - sqlite::Value::Real(r) => v1::Value::Real(r), - sqlite::Value::Text(t) => v1::Value::Text(t), - sqlite::Value::Blob(b) => v1::Value::Blob(b), - sqlite::Value::Null => v1::Value::Null, - } -} - -fn from_legacy_value(value: v1::Value) -> sqlite::Value { - match value { - v1::Value::Integer(i) => sqlite::Value::Integer(i), - v1::Value::Real(r) => sqlite::Value::Real(r), - v1::Value::Text(t) => sqlite::Value::Text(t), - v1::Value::Blob(b) => sqlite::Value::Blob(b), - v1::Value::Null => sqlite::Value::Null, - } -} From f45599a42b84f5504edcaf20f414e6f228aee2b8 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Wed, 28 Aug 2024 16:21:10 +0200 Subject: [PATCH 2/3] Elimenate all references to spin-sqlite Signed-off-by: Ryan Levick --- Cargo.lock | 1 - crates/factor-sqlite/Cargo.toml | 6 +++--- .../factor-sqlite/src/runtime_config/spin.rs | 19 +++++++------------ crates/sqlite-inproc/src/lib.rs | 9 +++------ crates/sqlite-libsql/Cargo.toml | 1 - crates/sqlite-libsql/src/lib.rs | 7 +++---- crates/sqlite/src/lib.rs | 13 ------------- 7 files changed, 16 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf202e7a3..66c1f1639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7897,7 +7897,6 @@ dependencies = [ "async-trait", "libsql", "rusqlite", - "spin-sqlite", "spin-world", "sqlparser", "tokio", diff --git a/crates/factor-sqlite/Cargo.toml b/crates/factor-sqlite/Cargo.toml index 4919a7305..8047e0473 100644 --- a/crates/factor-sqlite/Cargo.toml +++ b/crates/factor-sqlite/Cargo.toml @@ -29,9 +29,9 @@ tokio = { version = "1", features = ["macros", "rt"] } default = ["spin-cli"] # Includes the runtime configuration handling used by the Spin CLI spin-cli = [ - "dep:spin-sqlite", - "dep:spin-sqlite-inproc", - "dep:spin-sqlite-libsql", + "dep:spin-sqlite", + "dep:spin-sqlite-inproc", + "dep:spin-sqlite-libsql", ] [lints] diff --git a/crates/factor-sqlite/src/runtime_config/spin.rs b/crates/factor-sqlite/src/runtime_config/spin.rs index ef84ca160..07604ac6e 100644 --- a/crates/factor-sqlite/src/runtime_config/spin.rs +++ b/crates/factor-sqlite/src/runtime_config/spin.rs @@ -5,6 +5,7 @@ use std::{ sync::Arc, }; +use async_trait::async_trait; use serde::Deserialize; use spin_factors::{ anyhow::{self, Context as _}, @@ -119,18 +120,18 @@ impl DefaultLabelResolver for RuntimeConfigResolver { const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db"; -#[async_trait::async_trait] +#[async_trait] impl Connection for spin_sqlite_inproc::InProcConnection { async fn query( &self, query: &str, parameters: Vec, ) -> Result { - ::query(self, query, parameters).await + self.query(query, parameters).await } async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { - ::execute_batch(self, statements).await + self.execute_batch(statements).await } } @@ -165,7 +166,7 @@ impl LibSqlConnection { } } -#[async_trait::async_trait] +#[async_trait] impl Connection for LibSqlConnection { async fn query( &self, @@ -173,18 +174,12 @@ impl Connection for LibSqlConnection { parameters: Vec, ) -> Result { let client = self.get_client().await?; - ::query( - client, query, parameters, - ) - .await + client.query(query, parameters).await } async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { let client = self.get_client().await?; - ::execute_batch( - client, statements, - ) - .await + client.execute_batch(statements).await } } diff --git a/crates/sqlite-inproc/src/lib.rs b/crates/sqlite-inproc/src/lib.rs index 63d24d8fb..0129f359c 100644 --- a/crates/sqlite-inproc/src/lib.rs +++ b/crates/sqlite-inproc/src/lib.rs @@ -4,9 +4,7 @@ use std::{ }; use anyhow::Context; -use async_trait::async_trait; use once_cell::sync::OnceCell; -use spin_sqlite::Connection; use spin_world::v2::sqlite; use tracing::{instrument, Level}; @@ -68,10 +66,9 @@ impl InProcConnection { } } -#[async_trait] -impl Connection for InProcConnection { +impl InProcConnection { #[instrument(name = "spin_sqlite_inproc.query", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", otel.name = query))] - async fn query( + pub async fn query( &self, query: &str, parameters: Vec, @@ -86,7 +83,7 @@ impl Connection for InProcConnection { } #[instrument(name = "spin_sqlite_inproc.execute_batch", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", db.statements = statements))] - async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { + pub async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { let connection = self.db_connection()?; let statements = statements.to_owned(); tokio::task::spawn_blocking(move || { diff --git a/crates/sqlite-libsql/Cargo.toml b/crates/sqlite-libsql/Cargo.toml index 0d2f27920..8203fecb8 100644 --- a/crates/sqlite-libsql/Cargo.toml +++ b/crates/sqlite-libsql/Cargo.toml @@ -11,7 +11,6 @@ async-trait = "0.1.68" # libsqlite3-sys as used by spin-sqlite-inproc. libsql = { version = "0.3.2", features = ["remote"], default-features = false } rusqlite = { version = "0.29.0", features = ["bundled"] } -spin-sqlite = { path = "../sqlite" } spin-world = { path = "../world" } sqlparser = "0.34" tokio = { version = "1", features = ["full"] } diff --git a/crates/sqlite-libsql/src/lib.rs b/crates/sqlite-libsql/src/lib.rs index c901f7a24..329b2914b 100644 --- a/crates/sqlite-libsql/src/lib.rs +++ b/crates/sqlite-libsql/src/lib.rs @@ -15,10 +15,9 @@ impl LibsqlClient { } } -#[async_trait::async_trait] -impl spin_sqlite::Connection for LibsqlClient { +impl LibsqlClient { #[instrument(name = "spin_sqlite_libsql.query", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", otel.name = query))] - async fn query( + pub async fn query( &self, query: &str, parameters: Vec, @@ -38,7 +37,7 @@ impl spin_sqlite::Connection for LibsqlClient { } #[instrument(name = "spin_sqlite_libsql.execute_batch", skip(self), err(level = Level::INFO), fields(otel.kind = "client", db.system = "sqlite", db.statements = statements))] - async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { + pub async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { self.inner.execute_batch(statements).await?; Ok(()) diff --git a/crates/sqlite/src/lib.rs b/crates/sqlite/src/lib.rs index 2ea4de250..8b1378917 100644 --- a/crates/sqlite/src/lib.rs +++ b/crates/sqlite/src/lib.rs @@ -1,14 +1 @@ -use spin_core::async_trait; -use spin_world::v2::sqlite; -/// A trait abstracting over operations to a SQLite database -#[async_trait] -pub trait Connection: Send + Sync { - async fn query( - &self, - query: &str, - parameters: Vec, - ) -> Result; - - async fn execute_batch(&self, statements: &str) -> anyhow::Result<()>; -} From 3957b2975584d9acfd3e76fb9611cb7dcc7ef57a Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Wed, 28 Aug 2024 16:43:46 +0200 Subject: [PATCH 3/3] Repurpose spin-sqlite crate Signed-off-by: Ryan Levick --- Cargo.lock | 16 +- crates/factor-sqlite/Cargo.toml | 13 +- crates/factor-sqlite/src/runtime_config.rs | 3 - .../factor-sqlite/src/runtime_config/spin.rs | 257 ------------------ crates/factor-sqlite/tests/factor_test.rs | 3 +- crates/runtime-config/Cargo.toml | 1 + crates/runtime-config/src/lib.rs | 2 +- crates/sqlite-inproc/Cargo.toml | 4 +- crates/sqlite-inproc/src/lib.rs | 19 +- crates/sqlite/Cargo.toml | 13 +- crates/sqlite/src/lib.rs | 241 ++++++++++++++++ examples/spin-timer/Cargo.lock | 18 +- 12 files changed, 292 insertions(+), 298 deletions(-) delete mode 100644 crates/factor-sqlite/src/runtime_config/spin.rs diff --git a/Cargo.lock b/Cargo.lock index 66c1f1639..4b88a5fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7480,8 +7480,6 @@ dependencies = [ "spin-factors-test", "spin-locked-app", "spin-sqlite", - "spin-sqlite-inproc", - "spin-sqlite-libsql", "spin-world", "table", "tokio", @@ -7846,6 +7844,7 @@ dependencies = [ "spin-factor-variables", "spin-factor-wasi", "spin-factors", + "spin-sqlite", "toml 0.8.14", ] @@ -7864,14 +7863,17 @@ dependencies = [ name = "spin-sqlite" version = "2.8.0-pre0" dependencies = [ - "anyhow", "async-trait", - "spin-app", - "spin-core", + "serde 1.0.197", + "spin-factor-sqlite", + "spin-factors", + "spin-locked-app", + "spin-sqlite-inproc", + "spin-sqlite-libsql", "spin-world", "table", "tokio", - "tracing", + "toml 0.8.14", ] [[package]] @@ -7883,7 +7885,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "rusqlite", - "spin-sqlite", + "spin-factor-sqlite", "spin-world", "tokio", "tracing", diff --git a/crates/factor-sqlite/Cargo.toml b/crates/factor-sqlite/Cargo.toml index 8047e0473..1d7ba6bba 100644 --- a/crates/factor-sqlite/Cargo.toml +++ b/crates/factor-sqlite/Cargo.toml @@ -13,9 +13,6 @@ async-trait = "0.1" serde = { version = "1.0", features = ["rc"] } spin-factors = { path = "../factors" } spin-locked-app = { path = "../locked-app" } -spin-sqlite = { path = "../sqlite", optional = true } -spin-sqlite-inproc = { path = "../sqlite-inproc", optional = true } -spin-sqlite-libsql = { path = "../sqlite-libsql", optional = true } spin-world = { path = "../world" } table = { path = "../table" } tokio = "1" @@ -23,16 +20,8 @@ toml = "0.8" [dev-dependencies] spin-factors-test = { path = "../factors-test" } +spin-sqlite = { path = "../sqlite" } tokio = { version = "1", features = ["macros", "rt"] } -[features] -default = ["spin-cli"] -# Includes the runtime configuration handling used by the Spin CLI -spin-cli = [ - "dep:spin-sqlite", - "dep:spin-sqlite-inproc", - "dep:spin-sqlite-libsql", -] - [lints] workspace = true diff --git a/crates/factor-sqlite/src/runtime_config.rs b/crates/factor-sqlite/src/runtime_config.rs index 10eb8e871..5ccee194d 100644 --- a/crates/factor-sqlite/src/runtime_config.rs +++ b/crates/factor-sqlite/src/runtime_config.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "spin-cli")] -pub mod spin; - use std::{collections::HashMap, sync::Arc}; use crate::ConnectionCreator; diff --git a/crates/factor-sqlite/src/runtime_config/spin.rs b/crates/factor-sqlite/src/runtime_config/spin.rs deleted file mode 100644 index 07604ac6e..000000000 --- a/crates/factor-sqlite/src/runtime_config/spin.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Spin's default handling of the runtime configuration for SQLite databases. - -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use async_trait::async_trait; -use serde::Deserialize; -use spin_factors::{ - anyhow::{self, Context as _}, - runtime_config::toml::GetTomlValue, -}; -use spin_sqlite_inproc::InProcDatabaseLocation; -use spin_world::v2::sqlite as v2; -use tokio::sync::OnceCell; - -use crate::{Connection, ConnectionCreator, DefaultLabelResolver}; - -/// Spin's default resolution of runtime configuration for SQLite databases. -/// -/// This type implements how Spin CLI's SQLite implementation is configured -/// through the runtime config toml as well as the behavior of the "default" label. -pub struct RuntimeConfigResolver { - default_database_dir: Option, - local_database_dir: PathBuf, -} - -impl RuntimeConfigResolver { - /// Create a new `SpinSqliteRuntimeConfig` - /// - /// This takes as arguments: - /// * the directory to use as the default location for SQLite databases. - /// Usually this will be the path to the `.spin` state directory. If - /// `None`, the default database will be in-memory. - /// * the path to the directory from which relative paths to - /// local SQLite databases are resolved. (this should most likely be the - /// path to the runtime-config file or the current working dir). - pub fn new(default_database_dir: Option, local_database_dir: PathBuf) -> Self { - Self { - default_database_dir, - local_database_dir, - } - } - - /// Get the runtime configuration for SQLite databases from a TOML table. - /// - /// Expects table to be in the format: - /// ````toml - /// [sqlite_database.$database-label] - /// type = "$database-type" - /// ... extra type specific configuration ... - /// ``` - pub fn resolve_from_toml( - &self, - table: &impl GetTomlValue, - ) -> anyhow::Result> { - let Some(table) = table.get("sqlite_database") else { - return Ok(None); - }; - let config: std::collections::HashMap = table.clone().try_into()?; - let connection_creators = config - .into_iter() - .map(|(k, v)| Ok((k, self.get_connection_creator(v)?))) - .collect::>()?; - Ok(Some(super::RuntimeConfig { - connection_creators, - })) - } - - /// Get a connection creator for a given runtime configuration. - pub fn get_connection_creator( - &self, - config: RuntimeConfig, - ) -> anyhow::Result> { - let database_kind = config.type_.as_str(); - match database_kind { - "spin" => { - let config: LocalDatabase = config.config.try_into()?; - Ok(Arc::new( - config.connection_creator(&self.local_database_dir)?, - )) - } - "libsql" => { - let config: LibSqlDatabase = config.config.try_into()?; - Ok(Arc::new(config.connection_creator()?)) - } - _ => anyhow::bail!("Unknown database kind: {database_kind}"), - } - } -} - -#[derive(Deserialize)] -pub struct RuntimeConfig { - #[serde(rename = "type")] - pub type_: String, - #[serde(flatten)] - pub config: toml::Table, -} - -impl DefaultLabelResolver for RuntimeConfigResolver { - fn default(&self, label: &str) -> Option> { - // Only default the database labeled "default". - if label != "default" { - return None; - } - - let path = self - .default_database_dir - .as_deref() - .map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME)); - let factory = move || { - let location = InProcDatabaseLocation::from_path(path.clone())?; - let connection = spin_sqlite_inproc::InProcConnection::new(location)?; - Ok(Box::new(connection) as _) - }; - Some(Arc::new(factory)) - } -} - -const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db"; - -#[async_trait] -impl Connection for spin_sqlite_inproc::InProcConnection { - async fn query( - &self, - query: &str, - parameters: Vec, - ) -> Result { - self.query(query, parameters).await - } - - async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { - self.execute_batch(statements).await - } -} - -/// A wrapper around a libSQL connection that implements the [`Connection`] trait. -struct LibSqlConnection { - url: String, - token: String, - // Since the libSQL client can only be created asynchronously, we wait until - // we're in the `Connection` implementation to create. Since we only want to do - // this once, we use a `OnceCell` to store it. - inner: OnceCell, -} - -impl LibSqlConnection { - fn new(url: String, token: String) -> Self { - Self { - url, - token, - inner: OnceCell::new(), - } - } - - async fn get_client(&self) -> Result<&spin_sqlite_libsql::LibsqlClient, v2::Error> { - self.inner - .get_or_try_init(|| async { - spin_sqlite_libsql::LibsqlClient::create(self.url.clone(), self.token.clone()) - .await - .context("failed to create SQLite client") - }) - .await - .map_err(|_| v2::Error::InvalidConnection) - } -} - -#[async_trait] -impl Connection for LibSqlConnection { - async fn query( - &self, - query: &str, - parameters: Vec, - ) -> Result { - let client = self.get_client().await?; - client.query(query, parameters).await - } - - async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { - let client = self.get_client().await?; - client.execute_batch(statements).await - } -} - -/// Configuration for a local SQLite database. -#[derive(Clone, Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct LocalDatabase { - pub path: Option, -} - -impl LocalDatabase { - /// Get a new connection creator for a local database. - /// - /// `base_dir` is the base directory path from which `path` is resolved if it is a relative path. - fn connection_creator(self, base_dir: &Path) -> anyhow::Result { - let path = self - .path - .as_ref() - .map(|p| resolve_relative_path(p, base_dir)); - let location = InProcDatabaseLocation::from_path(path)?; - let factory = move || { - let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?; - Ok(Box::new(connection) as _) - }; - Ok(factory) - } -} - -/// Resolve a relative path against a base dir. -/// -/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir. -fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf { - if path.is_absolute() { - return path.to_owned(); - } - base_dir.join(path) -} - -/// Configuration for a libSQL database. -#[derive(Clone, Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct LibSqlDatabase { - url: String, - token: String, -} - -impl LibSqlDatabase { - /// Get a new connection creator for a libSQL database. - fn connection_creator(self) -> anyhow::Result { - let url = check_url(&self.url) - .with_context(|| { - format!( - "unexpected libSQL URL '{}' in runtime config file ", - self.url - ) - })? - .to_owned(); - let factory = move || { - let connection = LibSqlConnection::new(url.clone(), self.token.clone()); - Ok(Box::new(connection) as _) - }; - Ok(factory) - } -} - -// Checks an incoming url is in the shape we expect -fn check_url(url: &str) -> anyhow::Result<&str> { - if url.starts_with("https://") || url.starts_with("http://") { - Ok(url) - } else { - Err(anyhow::anyhow!( - "URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)" - )) - } -} diff --git a/crates/factor-sqlite/tests/factor_test.rs b/crates/factor-sqlite/tests/factor_test.rs index b668bf343..9d1b63a57 100644 --- a/crates/factor-sqlite/tests/factor_test.rs +++ b/crates/factor-sqlite/tests/factor_test.rs @@ -1,12 +1,13 @@ use std::{collections::HashSet, sync::Arc}; -use spin_factor_sqlite::{runtime_config::spin::RuntimeConfigResolver, SqliteFactor}; +use spin_factor_sqlite::SqliteFactor; use spin_factors::{ anyhow::{self, bail, Context}, runtime_config::toml::TomlKeyTracker, Factor, FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, RuntimeFactors, }; use spin_factors_test::{toml, TestEnvironment}; +use spin_sqlite::RuntimeConfigResolver; #[derive(RuntimeFactors)] struct TestFactors { diff --git a/crates/runtime-config/Cargo.toml b/crates/runtime-config/Cargo.toml index 9ca8e5e8d..6efac61b5 100644 --- a/crates/runtime-config/Cargo.toml +++ b/crates/runtime-config/Cargo.toml @@ -25,6 +25,7 @@ spin-factor-sqlite = { path = "../factor-sqlite" } spin-factor-variables = { path = "../factor-variables" } spin-factor-wasi = { path = "../factor-wasi" } spin-factors = { path = "../factors" } +spin-sqlite = { path = "../sqlite" } toml = "0.8" [lints] diff --git a/crates/runtime-config/src/lib.rs b/crates/runtime-config/src/lib.rs index 362812c09..2691ec4aa 100644 --- a/crates/runtime-config/src/lib.rs +++ b/crates/runtime-config/src/lib.rs @@ -12,7 +12,6 @@ use spin_factor_outbound_networking::runtime_config::spin::SpinTlsRuntimeConfig; use spin_factor_outbound_networking::OutboundNetworkingFactor; use spin_factor_outbound_pg::OutboundPgFactor; use spin_factor_outbound_redis::OutboundRedisFactor; -use spin_factor_sqlite::runtime_config::spin as sqlite; use spin_factor_sqlite::SqliteFactor; use spin_factor_variables::{spin_cli as variables, VariablesFactor}; use spin_factor_wasi::WasiFactor; @@ -20,6 +19,7 @@ use spin_factors::runtime_config::toml::GetTomlValue as _; use spin_factors::{ runtime_config::toml::TomlKeyTracker, FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, }; +use spin_sqlite as sqlite; /// The default state directory for the trigger. pub const DEFAULT_STATE_DIR: &str = ".spin"; diff --git a/crates/sqlite-inproc/Cargo.toml b/crates/sqlite-inproc/Cargo.toml index 12fe8fc55..5c891b446 100644 --- a/crates/sqlite-inproc/Cargo.toml +++ b/crates/sqlite-inproc/Cargo.toml @@ -6,11 +6,11 @@ edition = { workspace = true } [dependencies] anyhow = "1.0" -async-trait = "0.1.68" +async-trait = "0.1" once_cell = "1" rand = "0.8" rusqlite = { version = "0.29.0", features = ["bundled"] } -spin-sqlite = { path = "../sqlite" } +spin-factor-sqlite = { path = "../factor-sqlite" } spin-world = { path = "../world" } tokio = "1" tracing = { workspace = true } diff --git a/crates/sqlite-inproc/src/lib.rs b/crates/sqlite-inproc/src/lib.rs index 0129f359c..c972341c8 100644 --- a/crates/sqlite-inproc/src/lib.rs +++ b/crates/sqlite-inproc/src/lib.rs @@ -3,8 +3,10 @@ use std::{ sync::{Arc, Mutex}, }; -use anyhow::Context; +use anyhow::Context as _; +use async_trait::async_trait; use once_cell::sync::OnceCell; +use spin_factor_sqlite::Connection; use spin_world::v2::sqlite; use tracing::{instrument, Level}; @@ -97,6 +99,21 @@ impl InProcConnection { } } +#[async_trait] +impl Connection for InProcConnection { + async fn query( + &self, + query: &str, + parameters: Vec, + ) -> Result { + self.query(query, parameters).await + } + + async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { + self.execute_batch(statements).await + } +} + fn execute_query( connection: &Mutex, query: &str, diff --git a/crates/sqlite/Cargo.toml b/crates/sqlite/Cargo.toml index 0cb7b12b4..27e070740 100644 --- a/crates/sqlite/Cargo.toml +++ b/crates/sqlite/Cargo.toml @@ -5,11 +5,14 @@ authors = { workspace = true } edition = { workspace = true } [dependencies] -anyhow = "1.0" -async-trait = "0.1.68" -spin-app = { path = "../app" } -spin-core = { path = "../core" } +async-trait = "0.1" +serde = { version = "1.0", features = ["rc"] } +spin-factor-sqlite = { path = "../factor-sqlite" } +spin-factors = { path = "../factors" } +spin-locked-app = { path = "../locked-app" } +spin-sqlite-inproc = { path = "../sqlite-inproc" } +spin-sqlite-libsql = { path = "../sqlite-libsql" } spin-world = { path = "../world" } table = { path = "../table" } tokio = "1" -tracing = { workspace = true } +toml = "0.8" diff --git a/crates/sqlite/src/lib.rs b/crates/sqlite/src/lib.rs index 8b1378917..a7d347d9c 100644 --- a/crates/sqlite/src/lib.rs +++ b/crates/sqlite/src/lib.rs @@ -1 +1,242 @@ +//! Spin's default handling of the runtime configuration for SQLite databases. +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use async_trait::async_trait; +use serde::Deserialize; +use spin_factor_sqlite::{Connection, ConnectionCreator, DefaultLabelResolver}; +use spin_factors::{ + anyhow::{self, Context as _}, + runtime_config::toml::GetTomlValue, +}; +use spin_sqlite_inproc::InProcDatabaseLocation; +use spin_world::v2::sqlite as v2; +use tokio::sync::OnceCell; + +/// Spin's default resolution of runtime configuration for SQLite databases. +/// +/// This type implements how Spin CLI's SQLite implementation is configured +/// through the runtime config toml as well as the behavior of the "default" label. +pub struct RuntimeConfigResolver { + default_database_dir: Option, + local_database_dir: PathBuf, +} + +impl RuntimeConfigResolver { + /// Create a new `SpinSqliteRuntimeConfig` + /// + /// This takes as arguments: + /// * the directory to use as the default location for SQLite databases. + /// Usually this will be the path to the `.spin` state directory. If + /// `None`, the default database will be in-memory. + /// * the path to the directory from which relative paths to + /// local SQLite databases are resolved. (this should most likely be the + /// path to the runtime-config file or the current working dir). + pub fn new(default_database_dir: Option, local_database_dir: PathBuf) -> Self { + Self { + default_database_dir, + local_database_dir, + } + } + + /// Get the runtime configuration for SQLite databases from a TOML table. + /// + /// Expects table to be in the format: + /// ````toml + /// [sqlite_database.$database-label] + /// type = "$database-type" + /// ... extra type specific configuration ... + /// ``` + pub fn resolve_from_toml( + &self, + table: &impl GetTomlValue, + ) -> anyhow::Result> { + let Some(table) = table.get("sqlite_database") else { + return Ok(None); + }; + let config: std::collections::HashMap = + table.clone().try_into()?; + let connection_creators = config + .into_iter() + .map(|(k, v)| Ok((k, self.get_connection_creator(v)?))) + .collect::>()?; + Ok(Some(spin_factor_sqlite::runtime_config::RuntimeConfig { + connection_creators, + })) + } + + /// Get a connection creator for a given runtime configuration. + pub fn get_connection_creator( + &self, + config: TomlRuntimeConfig, + ) -> anyhow::Result> { + let database_kind = config.type_.as_str(); + match database_kind { + "spin" => { + let config: LocalDatabase = config.config.try_into()?; + Ok(Arc::new( + config.connection_creator(&self.local_database_dir)?, + )) + } + "libsql" => { + let config: LibSqlDatabase = config.config.try_into()?; + Ok(Arc::new(config.connection_creator()?)) + } + _ => anyhow::bail!("Unknown database kind: {database_kind}"), + } + } +} + +#[derive(Deserialize)] +pub struct TomlRuntimeConfig { + #[serde(rename = "type")] + pub type_: String, + #[serde(flatten)] + pub config: toml::Table, +} + +impl DefaultLabelResolver for RuntimeConfigResolver { + fn default(&self, label: &str) -> Option> { + // Only default the database labeled "default". + if label != "default" { + return None; + } + + let path = self + .default_database_dir + .as_deref() + .map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME)); + let factory = move || { + let location = InProcDatabaseLocation::from_path(path.clone())?; + let connection = spin_sqlite_inproc::InProcConnection::new(location)?; + Ok(Box::new(connection) as _) + }; + Some(Arc::new(factory)) + } +} + +const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db"; + +/// A wrapper around a libSQL connection that implements the [`Connection`] trait. +struct LibSqlConnection { + url: String, + token: String, + // Since the libSQL client can only be created asynchronously, we wait until + // we're in the `Connection` implementation to create. Since we only want to do + // this once, we use a `OnceCell` to store it. + inner: OnceCell, +} + +impl LibSqlConnection { + fn new(url: String, token: String) -> Self { + Self { + url, + token, + inner: OnceCell::new(), + } + } + + async fn get_client(&self) -> Result<&spin_sqlite_libsql::LibsqlClient, v2::Error> { + self.inner + .get_or_try_init(|| async { + spin_sqlite_libsql::LibsqlClient::create(self.url.clone(), self.token.clone()) + .await + .context("failed to create SQLite client") + }) + .await + .map_err(|_| v2::Error::InvalidConnection) + } +} + +#[async_trait] +impl Connection for LibSqlConnection { + async fn query( + &self, + query: &str, + parameters: Vec, + ) -> Result { + let client = self.get_client().await?; + client.query(query, parameters).await + } + + async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { + let client = self.get_client().await?; + client.execute_batch(statements).await + } +} + +/// Configuration for a local SQLite database. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LocalDatabase { + pub path: Option, +} + +impl LocalDatabase { + /// Get a new connection creator for a local database. + /// + /// `base_dir` is the base directory path from which `path` is resolved if it is a relative path. + fn connection_creator(self, base_dir: &Path) -> anyhow::Result { + let path = self + .path + .as_ref() + .map(|p| resolve_relative_path(p, base_dir)); + let location = InProcDatabaseLocation::from_path(path)?; + let factory = move || { + let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?; + Ok(Box::new(connection) as _) + }; + Ok(factory) + } +} + +/// Resolve a relative path against a base dir. +/// +/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir. +fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf { + if path.is_absolute() { + return path.to_owned(); + } + base_dir.join(path) +} + +/// Configuration for a libSQL database. +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LibSqlDatabase { + url: String, + token: String, +} + +impl LibSqlDatabase { + /// Get a new connection creator for a libSQL database. + fn connection_creator(self) -> anyhow::Result { + let url = check_url(&self.url) + .with_context(|| { + format!( + "unexpected libSQL URL '{}' in runtime config file ", + self.url + ) + })? + .to_owned(); + let factory = move || { + let connection = LibSqlConnection::new(url.clone(), self.token.clone()); + Ok(Box::new(connection) as _) + }; + Ok(factory) + } +} + +// Checks an incoming url is in the shape we expect +fn check_url(url: &str) -> anyhow::Result<&str> { + if url.starts_with("https://") || url.starts_with("http://") { + Ok(url) + } else { + Err(anyhow::anyhow!( + "URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)" + )) + } +} diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index ecff76a6c..83d0f1cdf 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -3947,9 +3947,6 @@ dependencies = [ "serde", "spin-factors", "spin-locked-app", - "spin-sqlite", - "spin-sqlite-inproc", - "spin-sqlite-libsql", "spin-world", "table", "tokio", @@ -4139,6 +4136,7 @@ dependencies = [ "spin-factor-variables", "spin-factor-wasi", "spin-factors", + "spin-sqlite", "toml", ] @@ -4157,14 +4155,17 @@ dependencies = [ name = "spin-sqlite" version = "2.8.0-pre0" dependencies = [ - "anyhow", "async-trait", - "spin-app", - "spin-core", + "serde", + "spin-factor-sqlite", + "spin-factors", + "spin-locked-app", + "spin-sqlite-inproc", + "spin-sqlite-libsql", "spin-world", "table", "tokio", - "tracing", + "toml", ] [[package]] @@ -4176,7 +4177,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "rusqlite", - "spin-sqlite", + "spin-factor-sqlite", "spin-world", "tokio", "tracing", @@ -4190,7 +4191,6 @@ dependencies = [ "async-trait", "libsql", "rusqlite", - "spin-sqlite", "spin-world", "sqlparser", "tokio",