Skip to content

Commit 0708080

Browse files
committed
Run sqlite statements
Signed-off-by: Ryan Levick <[email protected]>
1 parent d156648 commit 0708080

File tree

5 files changed

+122
-13
lines changed

5 files changed

+122
-13
lines changed

build.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,6 @@ error: the `wasm32-wasi` target is not installed
6868
std::fs::create_dir_all("target/test-programs").unwrap();
6969

7070
build_wasm_test_program("core-wasi-test.wasm", "crates/core/tests/core-wasi-test");
71-
// build_wasm_test_program("redis-rust.wasm", "crates/trigger-redis/tests/rust");
72-
// build_wasm_test_program(
73-
// "spin-http-benchmark.wasm",
74-
// "crates/trigger-http/benches/spin-http-benchmark",
75-
// );
76-
// build_wasm_test_program(
77-
// "wagi-benchmark.wasm",
78-
// "crates/trigger-http/benches/wagi-benchmark",
79-
// );
80-
// build_wasm_test_program("timer_app_example.wasm", "examples/spin-timer/app-example");
8171

8272
cargo_build(TIMER_TRIGGER_INTEGRATION_TEST);
8373
}

crates/factor-sqlite/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,29 @@ pub trait DefaultLabelResolver: Send + Sync {
149149
fn default(&self, label: &str) -> Option<Arc<dyn ConnectionCreator>>;
150150
}
151151

152+
#[derive(Clone)]
152153
pub struct AppState {
153154
/// A map from component id to a set of allowed database labels.
154155
allowed_databases: HashMap<String, Arc<HashSet<String>>>,
155156
/// A function for mapping from database name to a connection creator.
156157
get_connection_creator: host::ConnectionCreatorGetter,
157158
}
158159

160+
impl AppState {
161+
/// Get a connection for a given database label.
162+
///
163+
/// Returns `None` if there is no connection creator for the given label.
164+
pub async fn get_connection(
165+
&self,
166+
label: &str,
167+
) -> Option<Result<Box<dyn Connection>, v2::Error>> {
168+
let connection = (self.get_connection_creator)(label)?
169+
.create_connection()
170+
.await;
171+
Some(connection)
172+
}
173+
}
174+
159175
/// A creator of a connections for a particular SQLite database.
160176
#[async_trait]
161177
pub trait ConnectionCreator: Send + Sync {

crates/trigger/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ spin-factors-executor = { path = "../factors-executor" }
4242
spin-runtime-config = { path = "../runtime-config" }
4343
spin-telemetry = { path = "../telemetry" }
4444
terminal = { path = "../terminal" }
45-
tokio = { version = "1.23", features = ["fs"] }
45+
tokio = { version = "1.23", features = ["fs", "rt"] }
4646
tracing = { workspace = true }
4747

4848
[lints]

crates/trigger/src/cli.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod launch_metadata;
2+
mod sqlite_statements;
23
mod summary;
34

45
use std::future::Future;
@@ -13,6 +14,7 @@ use spin_common::{arg_parser::parse_kv, sloth};
1314
use spin_core::async_trait;
1415
use spin_factors_executor::{ComponentLoader, FactorsExecutor};
1516
use spin_runtime_config::{ResolvedRuntimeConfig, UserProvidedPath};
17+
use sqlite_statements::SqlStatementExecutorHook;
1618
use summary::KeyValueDefaultStoreSummaryHook;
1719

1820
use crate::factors::{TriggerFactors, TriggerFactorsRuntimeConfig};
@@ -198,6 +200,7 @@ impl<T: Trigger> FactorsTriggerCommand<T> {
198200
state_dir: self.state_dir.as_deref(),
199201
local_app_dir: local_app_dir.as_deref(),
200202
initial_key_values: self.key_values,
203+
sqlite_statements: self.sqlite_statements,
201204
allow_transient_write: self.allow_transient_write,
202205
follow_components,
203206
log_dir: self.log,
@@ -277,6 +280,8 @@ pub struct TriggerAppOptions<'a> {
277280
local_app_dir: Option<&'a str>,
278281
/// Initial key/value pairs to set in the app's default store.
279282
initial_key_values: Vec<(String, String)>,
283+
/// SQLite statements to run.
284+
sqlite_statements: Vec<String>,
280285
/// Whether to allow transient writes to mounted files
281286
allow_transient_write: bool,
282287
/// Which components should have their logs followed.
@@ -351,8 +356,6 @@ impl<T: Trigger> TriggerAppBuilder<T> {
351356
)
352357
.context("failed to create factors")?;
353358

354-
// TODO(factors): handle: self.sqlite_statements
355-
356359
// TODO: port the rest of the component loader logic
357360
struct SimpleComponentLoader;
358361

@@ -421,6 +424,7 @@ impl<T: Trigger> TriggerAppBuilder<T> {
421424
// TODO:
422425
// builder.hooks(SummariseRuntimeConfigHook::new(&self.runtime_config_file));
423426
executor.add_hooks(KeyValueDefaultStoreSummaryHook);
427+
executor.add_hooks(SqlStatementExecutorHook::new(options.sqlite_statements));
424428
// builder.hooks(SqlitePersistenceMessageHook);
425429

426430
let configured_app = {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use anyhow::Context as _;
2+
use spin_factor_sqlite::SqliteFactor;
3+
use spin_factors::RuntimeFactors;
4+
use spin_factors_executor::ExecutorHooks;
5+
6+
/// The default sqlite label
7+
const DEFAULT_SQLITE_LABEL: &str = "default";
8+
9+
/// ExecutorHook for executing sqlite statements.
10+
///
11+
/// This executor assumes that the configured app has access to `SqliteFactor`.
12+
/// It will silently ignore the hook if the app does not have access to `SqliteFactor`.
13+
pub struct SqlStatementExecutorHook {
14+
sql_statements: Vec<String>,
15+
}
16+
17+
impl SqlStatementExecutorHook {
18+
/// Creates a new SqlStatementExecutorHook
19+
///
20+
/// The statements can be either a list of raw SQL statements or a list of `@{file:label}` statements.
21+
pub fn new(sql_statements: Vec<String>) -> Self {
22+
Self { sql_statements }
23+
}
24+
}
25+
26+
impl<F: RuntimeFactors, U> ExecutorHooks<F, U> for SqlStatementExecutorHook {
27+
fn configure_app(
28+
&mut self,
29+
configured_app: &spin_factors::ConfiguredApp<F>,
30+
) -> anyhow::Result<()> {
31+
if self.sql_statements.is_empty() {
32+
return Ok(());
33+
}
34+
let Some(sqlite) = configured_app.app_state::<SqliteFactor>().ok() else {
35+
return Ok(());
36+
};
37+
if let Ok(current) = tokio::runtime::Handle::try_current() {
38+
let _ = current.spawn(execute(sqlite.clone(), self.sql_statements.clone()));
39+
}
40+
Ok(())
41+
}
42+
}
43+
44+
/// Executes the sql statements.
45+
pub async fn execute(
46+
sqlite: spin_factor_sqlite::AppState,
47+
sql_statements: Vec<String>,
48+
) -> anyhow::Result<()> {
49+
let get_database = |label| {
50+
let sqlite = &sqlite;
51+
async move {
52+
sqlite
53+
.get_connection(label)
54+
.await
55+
.transpose()
56+
.with_context(|| format!("failed connect to database with label '{label}'"))
57+
}
58+
};
59+
60+
for statement in &sql_statements {
61+
if let Some(config) = statement.strip_prefix('@') {
62+
let (file, label) = parse_file_and_label(config)?;
63+
let database = get_database(label).await?.with_context(|| {
64+
format!(
65+
"based on the '@{config}' a registered database named '{label}' was expected but not found."
66+
)
67+
})?;
68+
let sql = std::fs::read_to_string(file).with_context(|| {
69+
format!("could not read file '{file}' containing sql statements")
70+
})?;
71+
database.execute_batch(&sql).await.with_context(|| {
72+
format!("failed to execute sql against database '{label}' from file '{file}'")
73+
})?;
74+
} else {
75+
let Some(default) = get_database(DEFAULT_SQLITE_LABEL).await? else {
76+
debug_assert!(false, "the '{DEFAULT_SQLITE_LABEL}' sqlite database should always be available but for some reason was not");
77+
return Ok(());
78+
};
79+
default
80+
.query(statement, Vec::new())
81+
.await
82+
.with_context(|| format!("failed to execute following sql statement against default database: '{statement}'"))?;
83+
}
84+
}
85+
Ok(())
86+
}
87+
88+
/// Parses a @{file:label} sqlite statement
89+
fn parse_file_and_label(config: &str) -> anyhow::Result<(&str, &str)> {
90+
let config = config.trim();
91+
let (file, label) = match config.split_once(':') {
92+
Some((_, label)) if label.trim().is_empty() => {
93+
anyhow::bail!("database label is empty in the '@{config}' sqlite statement")
94+
}
95+
Some((file, label)) => (file.trim(), label.trim()),
96+
None => (config, "default"),
97+
};
98+
Ok((file, label))
99+
}

0 commit comments

Comments
 (0)