mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 00:29:24 +00:00
feat: add libSQL/Turso database backend with full feature parity
Introduce a Database trait abstraction (~60 async methods) enabling compile-time backend selection between PostgreSQL and libSQL/Turso. Convert all modules from concrete Store to Arc<dyn Database>, add LibSqlSecretsStore and LibSqlWasmToolStore implementations, wire libsql stores throughout CLI and main entry points, and make the setup wizard backend-agnostic. Key changes: - src/db/: Database trait, PostgresDatabase adapter, LibSqlBackend with native SQLite-dialect SQL, and idempotent migration system - src/secrets/store.rs: LibSqlSecretsStore (all 8 trait methods) - src/tools/wasm/storage.rs: LibSqlWasmToolStore (all 7 trait methods) - src/main.rs, cli/tool.rs, cli/mcp.rs: backend-conditional wiring - src/setup/channels.rs: SecretsContext uses Arc<dyn SecretsStore> - Feature-gate postgres-only tests and examples Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
+86
-1
@@ -38,7 +38,7 @@ impl Config {
|
||||
/// Priority: env var > DB settings > default.
|
||||
/// This is the primary way to load config after DB is connected.
|
||||
pub async fn from_db(
|
||||
store: &crate::history::Store,
|
||||
store: &dyn crate::db::Database,
|
||||
user_id: &str,
|
||||
bootstrap: &crate::bootstrap::BootstrapConfig,
|
||||
) -> Result<Self, ConfigError> {
|
||||
@@ -134,17 +134,72 @@ impl TunnelConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which database backend to use.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum DatabaseBackend {
|
||||
/// PostgreSQL via deadpool-postgres (default).
|
||||
#[default]
|
||||
Postgres,
|
||||
/// libSQL/Turso embedded database.
|
||||
LibSql,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for DatabaseBackend {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"postgres" | "postgresql" | "pg" => Ok(Self::Postgres),
|
||||
"libsql" | "turso" | "sqlite" => Ok(Self::LibSql),
|
||||
_ => Err(format!(
|
||||
"invalid database backend '{}', expected 'postgres' or 'libsql'",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Database configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DatabaseConfig {
|
||||
/// Which backend to use (default: Postgres).
|
||||
pub backend: DatabaseBackend,
|
||||
|
||||
// -- PostgreSQL fields --
|
||||
pub url: SecretString,
|
||||
pub pool_size: usize,
|
||||
|
||||
// -- libSQL fields --
|
||||
/// Path to local libSQL database file (default: ~/.ironclaw/ironclaw.db).
|
||||
pub libsql_path: Option<PathBuf>,
|
||||
/// Turso cloud URL for remote sync (optional).
|
||||
pub libsql_url: Option<String>,
|
||||
/// Turso auth token (required when libsql_url is set).
|
||||
pub libsql_auth_token: Option<SecretString>,
|
||||
}
|
||||
|
||||
impl DatabaseConfig {
|
||||
fn resolve(bootstrap: &crate::bootstrap::BootstrapConfig) -> Result<Self, ConfigError> {
|
||||
let backend: DatabaseBackend = if let Some(b) = optional_env("DATABASE_BACKEND")? {
|
||||
b.parse().map_err(|e| ConfigError::InvalidValue {
|
||||
key: "DATABASE_BACKEND".to_string(),
|
||||
message: e,
|
||||
})?
|
||||
} else {
|
||||
DatabaseBackend::default()
|
||||
};
|
||||
|
||||
// PostgreSQL URL is required only when using the postgres backend.
|
||||
// For libsql backend, default to an empty placeholder.
|
||||
let url = optional_env("DATABASE_URL")?
|
||||
.or_else(|| bootstrap.database_url.clone())
|
||||
.or_else(|| {
|
||||
if backend == DatabaseBackend::LibSql {
|
||||
Some("unused://libsql".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| ConfigError::MissingRequired {
|
||||
key: "database_url".to_string(),
|
||||
hint: "Run 'ironclaw onboard' or set DATABASE_URL environment variable".to_string(),
|
||||
@@ -160,9 +215,31 @@ impl DatabaseConfig {
|
||||
.or(bootstrap.database_pool_size)
|
||||
.unwrap_or(10);
|
||||
|
||||
let libsql_path = optional_env("LIBSQL_PATH")?.map(PathBuf::from).or_else(|| {
|
||||
if backend == DatabaseBackend::LibSql {
|
||||
Some(default_libsql_path())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let libsql_url = optional_env("LIBSQL_URL")?;
|
||||
let libsql_auth_token = optional_env("LIBSQL_AUTH_TOKEN")?.map(SecretString::from);
|
||||
|
||||
if libsql_url.is_some() && libsql_auth_token.is_none() {
|
||||
return Err(ConfigError::MissingRequired {
|
||||
key: "LIBSQL_AUTH_TOKEN".to_string(),
|
||||
hint: "LIBSQL_AUTH_TOKEN is required when LIBSQL_URL is set".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
url: SecretString::from(url),
|
||||
pool_size,
|
||||
libsql_path,
|
||||
libsql_url,
|
||||
libsql_auth_token,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -172,6 +249,14 @@ impl DatabaseConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Default libSQL database path (~/.ironclaw/ironclaw.db).
|
||||
pub fn default_libsql_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironclaw")
|
||||
.join("ironclaw.db")
|
||||
}
|
||||
|
||||
/// Which LLM backend to use.
|
||||
///
|
||||
/// Defaults to `NearAi` to keep IronClaw close to the NEAR ecosystem.
|
||||
|
||||
Reference in New Issue
Block a user