feat(control-plane): add libSQL Turso backend
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
//! Each migration is a named step that runs inside a transaction.
|
||||
//! The `migrations` table tracks which steps have been applied.
|
||||
|
||||
use crate::error::ControlPlaneError;
|
||||
use rusqlite::{Connection, Result as SqlResult};
|
||||
|
||||
const MIGRATIONS: &[(&str, &str)] = &[
|
||||
@@ -72,6 +73,34 @@ pub fn run_migrations(conn: &Connection) -> SqlResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_libsql_migrations(conn: &libsql::Connection) -> Result<(), ControlPlaneError> {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS _migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut rows = conn.query("SELECT name FROM _migrations", ()).await?;
|
||||
let mut already_applied = Vec::new();
|
||||
while let Some(row) = rows.next().await? {
|
||||
already_applied.push(row.get::<String>(0)?);
|
||||
}
|
||||
|
||||
for (name, sql) in MIGRATIONS {
|
||||
if already_applied.iter().any(|value| value == name) {
|
||||
continue;
|
||||
}
|
||||
tracing::info!("Running libSQL migration: {name}");
|
||||
conn.execute_batch(sql).await?;
|
||||
conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [*name])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -96,6 +125,22 @@ mod tests {
|
||||
> 0
|
||||
}
|
||||
|
||||
#[cfg(feature = "turso-unit-tests")]
|
||||
async fn libsql_table_exists(conn: &libsql::Connection, name: &str) -> bool {
|
||||
let mut rows = conn
|
||||
.query(
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?1",
|
||||
[name],
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("query libSQL table {name}"));
|
||||
if let Some(row) = rows.next().await.unwrap_or(None) {
|
||||
row.get::<i64>(0).unwrap_or(0) > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_is_idempotent() {
|
||||
let conn = fresh_db();
|
||||
@@ -136,4 +181,53 @@ mod tests {
|
||||
assert!(table_exists(&conn, "users"));
|
||||
assert!(table_exists(&conn, "_migrations"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "turso-unit-tests")]
|
||||
#[test]
|
||||
fn libsql_migration_is_idempotent() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("build tokio runtime");
|
||||
let database = rt
|
||||
.block_on(async { libsql::Builder::new_local(":memory:").build().await })
|
||||
.expect("build libSQL in-memory database");
|
||||
let conn = database.connect().expect("connect libSQL");
|
||||
|
||||
rt.block_on(async {
|
||||
run_libsql_migrations(&conn)
|
||||
.await
|
||||
.expect("first libSQL migration run");
|
||||
run_libsql_migrations(&conn)
|
||||
.await
|
||||
.expect("second libSQL migration run (idempotent)");
|
||||
});
|
||||
|
||||
let tables: &[&str] = &[
|
||||
"users",
|
||||
"auth_identities",
|
||||
"auth_sessions",
|
||||
"workspaces",
|
||||
"workspace_members",
|
||||
"directory_grants",
|
||||
"share_links",
|
||||
"sync_state",
|
||||
"ai_policies",
|
||||
"audit_log",
|
||||
"outbox_events",
|
||||
"legacy_id_map",
|
||||
"ai_runtime_runs",
|
||||
"ai_runtime_events",
|
||||
"sidebar_shortcuts",
|
||||
"user_ui_preferences",
|
||||
"user_navigation_recent",
|
||||
"ai_agent_profiles",
|
||||
"ai_agent_profile_grants",
|
||||
"ai_external_conversation_bindings",
|
||||
];
|
||||
for table in tables {
|
||||
let exists = rt.block_on(async { libsql_table_exists(&conn, table).await });
|
||||
assert!(exists, "libSQL table {table} should exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user