diff --git a/CLAUDE.md b/CLAUDE.md index 0c21238e..89e0856c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ bash scripts/ralph_e2e_short_rp_test.sh All tests create temp directories and clean up after themselves. They must NOT be run from the plugin repo root (safety check enforced). -**Storage runtime**: flowctl is libSQL-only (async, native vector search via `F32_BLOB(384)`). The `flowctl-db` rusqlite crate was deleted in fn-19 — `flowctl-db-lsql` is the sole storage crate. First build downloads the fastembed ONNX model (~130MB) to `.fastembed_cache/` for semantic memory search; subsequent builds/tests reuse the cache. +**Storage runtime**: flowctl is libSQL-only (async, native vector search via `F32_BLOB(384)`). The `flowctl-db` crate was rewritten from rusqlite to libsql in fn-19 and is the sole storage crate. First build downloads the fastembed ONNX model (~130MB) to `.fastembed_cache/` for semantic memory search; subsequent builds/tests reuse the cache. ## Code Quality diff --git a/flowctl/Cargo.lock b/flowctl/Cargo.lock index 5b494675..e1b92da4 100644 --- a/flowctl/Cargo.lock +++ b/flowctl/Cargo.lock @@ -1063,7 +1063,7 @@ dependencies = [ "clap_complete", "flowctl-core", "flowctl-daemon", - "flowctl-db-lsql", + "flowctl-db", "flowctl-scheduler", "flowctl-service", "libsql", @@ -1103,7 +1103,7 @@ dependencies = [ "bytes", "chrono", "flowctl-core", - "flowctl-db-lsql", + "flowctl-db", "flowctl-scheduler", "flowctl-service", "http-body-util", @@ -1124,7 +1124,7 @@ dependencies = [ ] [[package]] -name = "flowctl-db-lsql" +name = "flowctl-db" version = "0.1.0" dependencies = [ "chrono", @@ -1162,7 +1162,7 @@ version = "0.1.0" dependencies = [ "chrono", "flowctl-core", - "flowctl-db-lsql", + "flowctl-db", "libsql", "serde", "serde_json", diff --git a/flowctl/Cargo.toml b/flowctl/Cargo.toml index 0d7ce996..d269008d 100644 --- a/flowctl/Cargo.toml +++ b/flowctl/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "crates/flowctl-core", - "crates/flowctl-db-lsql", + "crates/flowctl-db", "crates/flowctl-scheduler", "crates/flowctl-service", "crates/flowctl-cli", @@ -74,7 +74,7 @@ trycmd = "0.15" # Internal crate references flowctl-core = { path = "crates/flowctl-core" } -flowctl-db-lsql = { path = "crates/flowctl-db-lsql" } +flowctl-db = { path = "crates/flowctl-db" } flowctl-scheduler = { path = "crates/flowctl-scheduler" } flowctl-service = { path = "crates/flowctl-service" } diff --git a/flowctl/crates/flowctl-cli/Cargo.toml b/flowctl/crates/flowctl-cli/Cargo.toml index 650cc5eb..55cbd875 100644 --- a/flowctl/crates/flowctl-cli/Cargo.toml +++ b/flowctl/crates/flowctl-cli/Cargo.toml @@ -16,7 +16,7 @@ daemon = ["dep:flowctl-daemon", "dep:axum", "dep:tower-http"] [dependencies] flowctl-core = { workspace = true } -flowctl-db-lsql = { workspace = true } +flowctl-db = { workspace = true } flowctl-service = { workspace = true } libsql = { workspace = true } flowctl-scheduler = { workspace = true } @@ -42,5 +42,5 @@ trycmd = { workspace = true } tempfile = "3" serde_json = { workspace = true } flowctl-core = { workspace = true } -flowctl-db-lsql = { workspace = true } +flowctl-db = { workspace = true } flowctl-service = { workspace = true } diff --git a/flowctl/crates/flowctl-cli/src/commands/db_shim.rs b/flowctl/crates/flowctl-cli/src/commands/db_shim.rs index dea4c02b..831a44cf 100644 --- a/flowctl/crates/flowctl-cli/src/commands/db_shim.rs +++ b/flowctl/crates/flowctl-cli/src/commands/db_shim.rs @@ -1,4 +1,4 @@ -//! Sync shim over `flowctl-db-lsql` (async libSQL) providing the same API +//! Sync shim over `flowctl-db` (async libSQL) providing the same API //! surface as the deprecated `flowctl-db` (rusqlite) crate. //! //! Every sync method spins up a per-call `tokio::runtime::Builder:: @@ -14,8 +14,8 @@ use std::path::{Path, PathBuf}; -pub use flowctl_db_lsql::{DbError, ReindexResult}; -pub use flowctl_db_lsql::metrics::{ +pub use flowctl_db::{DbError, ReindexResult}; +pub use flowctl_db::metrics::{ Bottleneck, DoraMetrics, EpicStats, Summary, TokenBreakdown, WeeklyTrend, }; @@ -43,16 +43,16 @@ fn block_on(fut: F) -> F::Output { // ── Pool functions ────────────────────────────────────────────────── pub fn resolve_state_dir(working_dir: &Path) -> Result { - flowctl_db_lsql::resolve_state_dir(working_dir) + flowctl_db::resolve_state_dir(working_dir) } pub fn resolve_db_path(working_dir: &Path) -> Result { - flowctl_db_lsql::resolve_db_path(working_dir) + flowctl_db::resolve_db_path(working_dir) } pub fn open(working_dir: &Path) -> Result { block_on(async { - let db = flowctl_db_lsql::open_async(working_dir).await?; + let db = flowctl_db::open_async(working_dir).await?; let conn = db.connect()?; // Leak the Database handle to keep it alive for the process lifetime. // (libsql Database drop closes the file.) @@ -62,7 +62,7 @@ pub fn open(working_dir: &Path) -> Result { } pub fn cleanup(conn: &Connection) -> Result { - block_on(flowctl_db_lsql::cleanup(&conn.inner())) + block_on(flowctl_db::cleanup(&conn.inner())) } pub fn reindex( @@ -70,7 +70,7 @@ pub fn reindex( flow_dir: &Path, state_dir: Option<&Path>, ) -> Result { - block_on(flowctl_db_lsql::reindex(&conn.inner(), flow_dir, state_dir)) + block_on(flowctl_db::reindex(&conn.inner(), flow_dir, state_dir)) } // ── Epic repository ──────────────────────────────────────────────── @@ -83,25 +83,25 @@ impl EpicRepo { } pub fn get(&self, id: &str) -> Result { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).get(id)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).get(id)) } pub fn get_with_body( &self, id: &str, ) -> Result<(flowctl_core::types::Epic, String), DbError> { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).get_with_body(id)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).get_with_body(id)) } pub fn list( &self, status: Option<&str>, ) -> Result, DbError> { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).list(status)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).list(status)) } pub fn upsert(&self, epic: &flowctl_core::types::Epic) -> Result<(), DbError> { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).upsert(epic)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).upsert(epic)) } pub fn upsert_with_body( @@ -109,7 +109,7 @@ impl EpicRepo { epic: &flowctl_core::types::Epic, body: &str, ) -> Result<(), DbError> { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).upsert_with_body(epic, body)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).upsert_with_body(epic, body)) } pub fn update_status( @@ -117,7 +117,7 @@ impl EpicRepo { id: &str, status: flowctl_core::types::EpicStatus, ) -> Result<(), DbError> { - block_on(flowctl_db_lsql::EpicRepo::new(self.0.clone()).update_status(id, status)) + block_on(flowctl_db::EpicRepo::new(self.0.clone()).update_status(id, status)) } } @@ -131,21 +131,21 @@ impl TaskRepo { } pub fn get(&self, id: &str) -> Result { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).get(id)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).get(id)) } pub fn get_with_body( &self, id: &str, ) -> Result<(flowctl_core::types::Task, String), DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).get_with_body(id)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).get_with_body(id)) } pub fn list_by_epic( &self, epic_id: &str, ) -> Result, DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).list_by_epic(epic_id)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).list_by_epic(epic_id)) } pub fn list_all( @@ -153,11 +153,11 @@ impl TaskRepo { status: Option<&str>, domain: Option<&str>, ) -> Result, DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).list_all(status, domain)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).list_all(status, domain)) } pub fn upsert(&self, task: &flowctl_core::types::Task) -> Result<(), DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).upsert(task)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).upsert(task)) } pub fn upsert_with_body( @@ -165,7 +165,7 @@ impl TaskRepo { task: &flowctl_core::types::Task, body: &str, ) -> Result<(), DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).upsert_with_body(task, body)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).upsert_with_body(task, body)) } pub fn update_status( @@ -173,7 +173,7 @@ impl TaskRepo { id: &str, status: flowctl_core::state_machine::Status, ) -> Result<(), DbError> { - block_on(flowctl_db_lsql::TaskRepo::new(self.0.clone()).update_status(id, status)) + block_on(flowctl_db::TaskRepo::new(self.0.clone()).update_status(id, status)) } } @@ -188,18 +188,18 @@ impl DepRepo { pub fn add_task_dep(&self, task_id: &str, depends_on: &str) -> Result<(), DbError> { block_on( - flowctl_db_lsql::DepRepo::new(self.0.clone()).add_task_dep(task_id, depends_on), + flowctl_db::DepRepo::new(self.0.clone()).add_task_dep(task_id, depends_on), ) } pub fn remove_task_dep(&self, task_id: &str, depends_on: &str) -> Result<(), DbError> { block_on( - flowctl_db_lsql::DepRepo::new(self.0.clone()).remove_task_dep(task_id, depends_on), + flowctl_db::DepRepo::new(self.0.clone()).remove_task_dep(task_id, depends_on), ) } pub fn list_task_deps(&self, task_id: &str) -> Result, DbError> { - block_on(flowctl_db_lsql::DepRepo::new(self.0.clone()).list_task_deps(task_id)) + block_on(flowctl_db::DepRepo::new(self.0.clone()).list_task_deps(task_id)) } /// Replace all deps for a task (delete-all + insert each). @@ -238,14 +238,14 @@ impl RuntimeRepo { &self, task_id: &str, ) -> Result, DbError> { - block_on(flowctl_db_lsql::RuntimeRepo::new(self.0.clone()).get(task_id)) + block_on(flowctl_db::RuntimeRepo::new(self.0.clone()).get(task_id)) } pub fn upsert( &self, state: &flowctl_core::types::RuntimeState, ) -> Result<(), DbError> { - block_on(flowctl_db_lsql::RuntimeRepo::new(self.0.clone()).upsert(state)) + block_on(flowctl_db::RuntimeRepo::new(self.0.clone()).upsert(state)) } } @@ -260,20 +260,20 @@ impl FileLockRepo { pub fn acquire(&self, file_path: &str, task_id: &str) -> Result<(), DbError> { block_on( - flowctl_db_lsql::FileLockRepo::new(self.0.clone()).acquire(file_path, task_id), + flowctl_db::FileLockRepo::new(self.0.clone()).acquire(file_path, task_id), ) } pub fn release_for_task(&self, task_id: &str) -> Result { - block_on(flowctl_db_lsql::FileLockRepo::new(self.0.clone()).release_for_task(task_id)) + block_on(flowctl_db::FileLockRepo::new(self.0.clone()).release_for_task(task_id)) } pub fn release_all(&self) -> Result { - block_on(flowctl_db_lsql::FileLockRepo::new(self.0.clone()).release_all()) + block_on(flowctl_db::FileLockRepo::new(self.0.clone()).release_all()) } pub fn check(&self, file_path: &str) -> Result, DbError> { - block_on(flowctl_db_lsql::FileLockRepo::new(self.0.clone()).check(file_path)) + block_on(flowctl_db::FileLockRepo::new(self.0.clone()).check(file_path)) } /// List all active locks: (file_path, task_id, locked_at). @@ -310,13 +310,13 @@ impl PhaseProgressRepo { pub fn get_completed(&self, task_id: &str) -> Result, DbError> { block_on( - flowctl_db_lsql::PhaseProgressRepo::new(self.0.clone()).get_completed(task_id), + flowctl_db::PhaseProgressRepo::new(self.0.clone()).get_completed(task_id), ) } pub fn mark_done(&self, task_id: &str, phase: &str) -> Result<(), DbError> { block_on( - flowctl_db_lsql::PhaseProgressRepo::new(self.0.clone()).mark_done(task_id, phase), + flowctl_db::PhaseProgressRepo::new(self.0.clone()).mark_done(task_id, phase), ) } } @@ -331,33 +331,33 @@ impl StatsQuery { } pub fn summary(&self) -> Result { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).summary()) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).summary()) } pub fn per_epic(&self, epic_id: Option<&str>) -> Result, DbError> { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).epic_stats(epic_id)) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).epic_stats(epic_id)) } pub fn weekly_trends(&self, weeks: u32) -> Result, DbError> { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).weekly_trends(weeks)) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).weekly_trends(weeks)) } pub fn token_breakdown( &self, epic_id: Option<&str>, ) -> Result, DbError> { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).token_breakdown(epic_id)) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).token_breakdown(epic_id)) } pub fn bottlenecks(&self, limit: usize) -> Result, DbError> { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).bottlenecks(limit)) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).bottlenecks(limit)) } pub fn dora_metrics(&self) -> Result { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).dora_metrics()) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).dora_metrics()) } pub fn generate_monthly_rollups(&self) -> Result { - block_on(flowctl_db_lsql::StatsQuery::new(self.0.clone()).generate_monthly_rollups()) + block_on(flowctl_db::StatsQuery::new(self.0.clone()).generate_monthly_rollups()) } } diff --git a/flowctl/crates/flowctl-cli/src/commands/mcp.rs b/flowctl/crates/flowctl-cli/src/commands/mcp.rs index 9f487aaa..dd7fb321 100644 --- a/flowctl/crates/flowctl-cli/src/commands/mcp.rs +++ b/flowctl/crates/flowctl-cli/src/commands/mcp.rs @@ -191,7 +191,7 @@ fn mcp_context() -> Result<(PathBuf, Option), String> { .build() .map_err(|e| format!("runtime: {e}"))?; let conn = rt.block_on(async { - let db = flowctl_db_lsql::open_async(&cwd).await.ok()?; + let db = flowctl_db::open_async(&cwd).await.ok()?; db.connect().ok() }); Ok((flow_dir, conn)) diff --git a/flowctl/crates/flowctl-cli/src/commands/workflow/mod.rs b/flowctl/crates/flowctl-cli/src/commands/workflow/mod.rs index ecf548fb..469b7773 100644 --- a/flowctl/crates/flowctl-cli/src/commands/workflow/mod.rs +++ b/flowctl/crates/flowctl-cli/src/commands/workflow/mod.rs @@ -52,7 +52,7 @@ pub(crate) fn try_open_lsql_conn() -> Option { .build() .ok()?; rt.block_on(async { - let db = flowctl_db_lsql::open_async(&cwd).await.ok()?; + let db = flowctl_db::open_async(&cwd).await.ok()?; db.connect().ok() }) } diff --git a/flowctl/crates/flowctl-cli/tests/export_import_test.rs b/flowctl/crates/flowctl-cli/tests/export_import_test.rs index 0262632f..9ee09dc1 100644 --- a/flowctl/crates/flowctl-cli/tests/export_import_test.rs +++ b/flowctl/crates/flowctl-cli/tests/export_import_test.rs @@ -3,7 +3,7 @@ //! Tests the DB → Markdown → DB path by: //! 1. Creating an in-memory DB with test data //! 2. Writing Markdown files using frontmatter::write -//! 3. Re-importing via flowctl_db_lsql::reindex +//! 3. Re-importing via flowctl_db::reindex //! 4. Verifying data matches use std::fs; @@ -58,9 +58,9 @@ async fn export_import_round_trip() { fs::create_dir_all(&flow_dir).unwrap(); // Step 1: Create DB with test data. - let (_db, conn) = flowctl_db_lsql::open_memory_async().await.unwrap(); - let epic_repo = flowctl_db_lsql::EpicRepo::new(conn.clone()); - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let (_db, conn) = flowctl_db::open_memory_async().await.unwrap(); + let epic_repo = flowctl_db::EpicRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let epic = make_test_epic("fn-50-roundtrip", "Round Trip Test"); let epic_body = "## Description\nThis is the epic body content."; @@ -96,8 +96,8 @@ async fn export_import_round_trip() { fs::write(tasks_dir.join("fn-50-roundtrip.1.md"), &tcontent).unwrap(); // Step 3: Import into a fresh DB. - let (_db2, conn2) = flowctl_db_lsql::open_memory_async().await.unwrap(); - let result = flowctl_db_lsql::reindex(&conn2, &flow_dir, None) + let (_db2, conn2) = flowctl_db::open_memory_async().await.unwrap(); + let result = flowctl_db::reindex(&conn2, &flow_dir, None) .await .unwrap(); @@ -105,12 +105,12 @@ async fn export_import_round_trip() { assert_eq!(result.tasks_indexed, 1); // Step 4: Verify data matches. - let repo2 = flowctl_db_lsql::EpicRepo::new(conn2.clone()); + let repo2 = flowctl_db::EpicRepo::new(conn2.clone()); let (reimported_epic, reimported_body) = repo2.get_with_body("fn-50-roundtrip").await.unwrap(); assert_eq!(reimported_epic.title, "Round Trip Test"); assert_eq!(reimported_body.trim(), epic_body.trim()); - let trepo2 = flowctl_db_lsql::TaskRepo::new(conn2); + let trepo2 = flowctl_db::TaskRepo::new(conn2); let (reimported_task, reimported_tbody) = trepo2.get_with_body("fn-50-roundtrip.1").await.unwrap(); assert_eq!(reimported_task.title, "First Task"); @@ -126,8 +126,8 @@ async fn export_empty_db_produces_no_files() { fs::create_dir_all(&epics_dir).unwrap(); fs::create_dir_all(&tasks_dir).unwrap(); - let (_db, conn) = flowctl_db_lsql::open_memory_async().await.unwrap(); - let epic_repo = flowctl_db_lsql::EpicRepo::new(conn); + let (_db, conn) = flowctl_db::open_memory_async().await.unwrap(); + let epic_repo = flowctl_db::EpicRepo::new(conn); let epics = epic_repo.list(None).await.unwrap(); assert!(epics.is_empty()); } diff --git a/flowctl/crates/flowctl-cli/tests/parity_test.rs b/flowctl/crates/flowctl-cli/tests/parity_test.rs index 53dbb2e9..1f35c2f9 100644 --- a/flowctl/crates/flowctl-cli/tests/parity_test.rs +++ b/flowctl/crates/flowctl-cli/tests/parity_test.rs @@ -373,9 +373,9 @@ fn db_task_status(work_dir: &Path, task_id: &str) -> String { .build() .unwrap(); rt.block_on(async { - let db = flowctl_db_lsql::open_async(work_dir).await.expect("open db"); + let db = flowctl_db::open_async(work_dir).await.expect("open db"); let conn = db.connect().expect("connect"); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let task = repo.get(task_id).await.expect("get task"); task.status.to_string() }) diff --git a/flowctl/crates/flowctl-daemon/Cargo.toml b/flowctl/crates/flowctl-daemon/Cargo.toml index 38082184..d81f5d18 100644 --- a/flowctl/crates/flowctl-daemon/Cargo.toml +++ b/flowctl/crates/flowctl-daemon/Cargo.toml @@ -19,7 +19,7 @@ webhook = ["dep:reqwest"] [dependencies] flowctl-core = { workspace = true } -flowctl-db-lsql = { workspace = true } +flowctl-db = { workspace = true } flowctl-service = { workspace = true } flowctl-scheduler = { workspace = true } libsql = { workspace = true } diff --git a/flowctl/crates/flowctl-daemon/src/handlers/common.rs b/flowctl/crates/flowctl-daemon/src/handlers/common.rs index 0bb755ab..540a7a23 100644 --- a/flowctl/crates/flowctl-daemon/src/handlers/common.rs +++ b/flowctl/crates/flowctl-daemon/src/handlers/common.rs @@ -38,9 +38,9 @@ impl IntoResponse for AppError { } } -impl From for AppError { - fn from(e: flowctl_db_lsql::DbError) -> Self { - use flowctl_db_lsql::DbError; +impl From for AppError { + fn from(e: flowctl_db::DbError) -> Self { + use flowctl_db::DbError; match e { DbError::NotFound(msg) => AppError::NotFound(msg), DbError::Constraint(msg) => AppError::InvalidInput(msg), diff --git a/flowctl/crates/flowctl-daemon/src/handlers/dag.rs b/flowctl/crates/flowctl-daemon/src/handlers/dag.rs index 7d71be95..54934146 100644 --- a/flowctl/crates/flowctl-daemon/src/handlers/dag.rs +++ b/flowctl/crates/flowctl-daemon/src/handlers/dag.rs @@ -41,7 +41,7 @@ pub async fn dag_handler( axum::extract::Query(params): axum::extract::Query, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let tasks = repo.list_by_epic(¶ms.epic_id).await?; if tasks.is_empty() { @@ -127,7 +127,7 @@ pub async fn dag_detail_handler( axum::extract::Path(epic_id): axum::extract::Path, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let tasks = repo.list_by_epic(&epic_id).await?; if tasks.is_empty() { @@ -177,7 +177,7 @@ pub async fn dag_mutate_handler( Json(body): Json, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let repo = flowctl_db::TaskRepo::new(conn.clone()); match body.action.as_str() { "add_dep" => { @@ -309,7 +309,7 @@ pub async fn add_dep_handler( Json(body): Json, ) -> Result<(StatusCode, Json), AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let repo = flowctl_db::TaskRepo::new(conn.clone()); let from_task = repo.get(&body.from).await .map_err(|_| AppError::NotFound(format!("task not found: {}", body.from)))?; diff --git a/flowctl/crates/flowctl-daemon/src/handlers/epic.rs b/flowctl/crates/flowctl-daemon/src/handlers/epic.rs index 86fcd0f8..848d63c1 100644 --- a/flowctl/crates/flowctl-daemon/src/handlers/epic.rs +++ b/flowctl/crates/flowctl-daemon/src/handlers/epic.rs @@ -56,7 +56,7 @@ pub async fn create_epic_handler( updated_at: chrono::Utc::now(), }; - let repo = flowctl_db_lsql::EpicRepo::new(conn); + let repo = flowctl_db::EpicRepo::new(conn); repo.upsert(&epic).await?; Ok(( @@ -74,7 +74,7 @@ pub async fn set_epic_plan_handler( Json(body): Json, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::EpicRepo::new(conn.clone()); + let repo = flowctl_db::EpicRepo::new(conn.clone()); // Verify epic exists. let epic = repo @@ -123,7 +123,7 @@ pub async fn start_epic_work_handler( axum::extract::Path(epic_id): axum::extract::Path, ) -> Result, AppError> { let conn = state.db.clone(); - let epic_repo = flowctl_db_lsql::EpicRepo::new(conn.clone()); + let epic_repo = flowctl_db::EpicRepo::new(conn.clone()); // Verify epic exists. let _epic = epic_repo @@ -132,7 +132,7 @@ pub async fn start_epic_work_handler( .map_err(|_| AppError::NotFound(format!("epic not found: {epic_id}")))?; // Precondition: epic must have tasks. - let task_repo = flowctl_db_lsql::TaskRepo::new(conn); + let task_repo = flowctl_db::TaskRepo::new(conn); let tasks = task_repo.list_by_epic(&epic_id).await?; if tasks.is_empty() { return Err(AppError::InvalidInput(format!( diff --git a/flowctl/crates/flowctl-daemon/src/handlers/mod.rs b/flowctl/crates/flowctl-daemon/src/handlers/mod.rs index b2abdec0..a263a40e 100644 --- a/flowctl/crates/flowctl-daemon/src/handlers/mod.rs +++ b/flowctl/crates/flowctl-daemon/src/handlers/mod.rs @@ -73,7 +73,7 @@ pub async fn epics_handler( State(state): State, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::EpicRepo::new(conn); + let repo = flowctl_db::EpicRepo::new(conn); let epics = repo.list(None).await?; let value = serde_json::to_value(&epics) .map_err(|e| AppError::Internal(format!("serialization error: {e}")))?; @@ -86,7 +86,7 @@ pub async fn tasks_handler( axum::extract::Query(params): axum::extract::Query, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let tasks = if let Some(ref epic_id) = params.epic_id { repo.list_by_epic(epic_id).await? } else { @@ -123,7 +123,7 @@ pub async fn tokens_handler( axum::extract::Query(params): axum::extract::Query, ) -> Result, AppError> { let conn = state.db.clone(); - let log = flowctl_db_lsql::EventLog::new(conn); + let log = flowctl_db::EventLog::new(conn); if let Some(ref task_id) = params.task_id { let rows = log.tokens_by_task(task_id).await?; @@ -215,7 +215,7 @@ pub async fn stats_handler( State(state): State, ) -> Result, AppError> { let conn = state.db.clone(); - let stats = flowctl_db_lsql::StatsQuery::new(conn); + let stats = flowctl_db::StatsQuery::new(conn); let summary = stats.summary().await?; let value = serde_json::to_value(&summary) .map_err(|e| AppError::Internal(format!("serialization error: {e}")))?; diff --git a/flowctl/crates/flowctl-daemon/src/handlers/task.rs b/flowctl/crates/flowctl-daemon/src/handlers/task.rs index 3940158f..13b24354 100644 --- a/flowctl/crates/flowctl-daemon/src/handlers/task.rs +++ b/flowctl/crates/flowctl-daemon/src/handlers/task.rs @@ -49,7 +49,7 @@ pub async fn create_task_handler( created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); repo.upsert_with_body(&task, &body.body.unwrap_or_default()).await?; Ok(( StatusCode::CREATED, @@ -228,7 +228,7 @@ pub async fn skip_task_rest_handler( Json(_body): Json, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let task = repo .get(&task_id) .await @@ -297,7 +297,7 @@ pub async fn skip_task_handler( Json(body): Json, ) -> Result, AppError> { let conn = state.db.clone(); - let repo = flowctl_db_lsql::TaskRepo::new(conn); + let repo = flowctl_db::TaskRepo::new(conn); let task = repo .get(&body.task_id) .await @@ -393,19 +393,19 @@ pub async fn get_task_handler( axum::extract::Path(task_id): axum::extract::Path, ) -> Result, AppError> { let conn = state.db.clone(); - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let (task, body) = task_repo .get_with_body(&task_id) .await .map_err(|_| AppError::NotFound(format!("task not found: {task_id}")))?; - let evidence_repo = flowctl_db_lsql::EvidenceRepo::new(conn.clone()); + let evidence_repo = flowctl_db::EvidenceRepo::new(conn.clone()); let evidence = evidence_repo .get(&task_id) .await .map_err(|e| AppError::Internal(format!("evidence fetch error: {e}")))?; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn); let runtime = runtime_repo .get(&task_id) .await diff --git a/flowctl/crates/flowctl-daemon/src/server.rs b/flowctl/crates/flowctl-daemon/src/server.rs index 162ff776..87454ba2 100644 --- a/flowctl/crates/flowctl-daemon/src/server.rs +++ b/flowctl/crates/flowctl-daemon/src/server.rs @@ -24,7 +24,7 @@ pub async fn create_state(runtime: DaemonRuntime, event_bus: flowctl_scheduler:: .parent() // .flow/ .and_then(|p| p.parent()) // project root .context("cannot resolve project root from state_dir")?; - let db = flowctl_db_lsql::open_async(working_dir) + let db = flowctl_db::open_async(working_dir) .await .with_context(|| format!("failed to open db in {}", working_dir.display()))?; let conn = db.connect().context("failed to connect to libsql db")?; diff --git a/flowctl/crates/flowctl-db-lsql/Cargo.toml b/flowctl/crates/flowctl-db/Cargo.toml similarity index 95% rename from flowctl/crates/flowctl-db-lsql/Cargo.toml rename to flowctl/crates/flowctl-db/Cargo.toml index fc234447..16aa2c7c 100644 --- a/flowctl/crates/flowctl-db-lsql/Cargo.toml +++ b/flowctl/crates/flowctl-db/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "flowctl-db-lsql" +name = "flowctl-db" version = "0.1.0" description = "Async libSQL storage layer for flowctl (successor to flowctl-db)" edition.workspace = true diff --git a/flowctl/crates/flowctl-db-lsql/src/error.rs b/flowctl/crates/flowctl-db/src/error.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/error.rs rename to flowctl/crates/flowctl-db/src/error.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/events.rs b/flowctl/crates/flowctl-db/src/events.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/events.rs rename to flowctl/crates/flowctl-db/src/events.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/indexer.rs b/flowctl/crates/flowctl-db/src/indexer.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/indexer.rs rename to flowctl/crates/flowctl-db/src/indexer.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/lib.rs b/flowctl/crates/flowctl-db/src/lib.rs similarity index 69% rename from flowctl/crates/flowctl-db-lsql/src/lib.rs rename to flowctl/crates/flowctl-db/src/lib.rs index 44804898..ce3b383c 100644 --- a/flowctl/crates/flowctl-db-lsql/src/lib.rs +++ b/flowctl/crates/flowctl-db/src/lib.rs @@ -1,8 +1,7 @@ -//! flowctl-db-lsql: Async libSQL storage layer for flowctl. +//! flowctl-db: Async libSQL storage layer for flowctl. //! -//! Successor to `flowctl-db` (rusqlite-based). All DB access is async, -//! Tokio-native. Memory table uses libSQL's native vector column -//! (`F32_BLOB(384)`) for semantic search via `vector_top_k`. +//! All DB access is async, Tokio-native. Memory table uses libSQL's native +//! vector column (`F32_BLOB(384)`) for semantic search via `vector_top_k`. //! //! # Architecture //! @@ -13,11 +12,10 @@ //! - **Connections are cheap clones.** `libsql::Connection` is `Send + Sync`, //! pass by value. Do not wrap in `Arc>`. //! -//! # Why a separate crate? +//! # History //! -//! libsql 0.9 cannot coexist with `rusqlite(bundled)` in the same test -//! binary — their C-level static init collides. Keeping the new stack in -//! its own crate gives clean test isolation during migration. +//! This crate was rewritten from rusqlite to libsql in fn-19 (April 2026). +//! The old rusqlite implementation is no longer available. pub mod error; pub mod events; diff --git a/flowctl/crates/flowctl-db-lsql/src/memory.rs b/flowctl/crates/flowctl-db/src/memory.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/memory.rs rename to flowctl/crates/flowctl-db/src/memory.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/metrics.rs b/flowctl/crates/flowctl-db/src/metrics.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/metrics.rs rename to flowctl/crates/flowctl-db/src/metrics.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/pool.rs b/flowctl/crates/flowctl-db/src/pool.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/pool.rs rename to flowctl/crates/flowctl-db/src/pool.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/repo.rs b/flowctl/crates/flowctl-db/src/repo.rs similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/repo.rs rename to flowctl/crates/flowctl-db/src/repo.rs diff --git a/flowctl/crates/flowctl-db-lsql/src/schema.sql b/flowctl/crates/flowctl-db/src/schema.sql similarity index 100% rename from flowctl/crates/flowctl-db-lsql/src/schema.sql rename to flowctl/crates/flowctl-db/src/schema.sql diff --git a/flowctl/crates/flowctl-service/Cargo.toml b/flowctl/crates/flowctl-service/Cargo.toml index e1e684c8..feb2ec44 100644 --- a/flowctl/crates/flowctl-service/Cargo.toml +++ b/flowctl/crates/flowctl-service/Cargo.toml @@ -8,7 +8,7 @@ license.workspace = true [dependencies] flowctl-core = { workspace = true } -flowctl-db-lsql = { workspace = true } +flowctl-db = { workspace = true } libsql = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/flowctl/crates/flowctl-service/src/connection.rs b/flowctl/crates/flowctl-service/src/connection.rs index 96d6e85e..b9e042e6 100644 --- a/flowctl/crates/flowctl-service/src/connection.rs +++ b/flowctl/crates/flowctl-service/src/connection.rs @@ -11,7 +11,7 @@ use crate::error::{ServiceError, ServiceResult}; /// File-backed connection provider using a working directory. /// -/// Wraps `flowctl_db_lsql::open_async()` so callers can re-open as needed. +/// Wraps `flowctl_db::open_async()` so callers can re-open as needed. #[derive(Debug, Clone)] pub struct FileConnectionProvider { working_dir: PathBuf, @@ -32,23 +32,23 @@ impl FileConnectionProvider { /// Open a new libSQL connection asynchronously. pub async fn connect(&self) -> ServiceResult { - let db = flowctl_db_lsql::open_async(&self.working_dir) + let db = flowctl_db::open_async(&self.working_dir) .await .map_err(ServiceError::from)?; db.connect().map_err(|e| { - ServiceError::DbError(flowctl_db_lsql::DbError::LibSql(e)) + ServiceError::DbError(flowctl_db::DbError::LibSql(e)) }) } } /// Open a connection asynchronously (convenience wrapper around -/// `flowctl_db_lsql::open_async`). +/// `flowctl_db::open_async`). pub async fn open_async(working_dir: &Path) -> ServiceResult { - let db = flowctl_db_lsql::open_async(working_dir) + let db = flowctl_db::open_async(working_dir) .await .map_err(ServiceError::from)?; db.connect() - .map_err(|e| ServiceError::DbError(flowctl_db_lsql::DbError::LibSql(e))) + .map_err(|e| ServiceError::DbError(flowctl_db::DbError::LibSql(e))) } #[cfg(test)] diff --git a/flowctl/crates/flowctl-service/src/error.rs b/flowctl/crates/flowctl-service/src/error.rs index 4260e1c7..dce48414 100644 --- a/flowctl/crates/flowctl-service/src/error.rs +++ b/flowctl/crates/flowctl-service/src/error.rs @@ -31,7 +31,7 @@ pub enum ServiceError { /// Underlying database error. #[error("database error: {0}")] - DbError(#[from] flowctl_db_lsql::DbError), + DbError(#[from] flowctl_db::DbError), /// I/O error (file reads, state directory operations). #[error("io error: {0}")] diff --git a/flowctl/crates/flowctl-service/src/lifecycle.rs b/flowctl/crates/flowctl-service/src/lifecycle.rs index 54513978..fd67ebb2 100644 --- a/flowctl/crates/flowctl-service/src/lifecycle.rs +++ b/flowctl/crates/flowctl-service/src/lifecycle.rs @@ -114,7 +114,7 @@ fn validate_task_id(id: &str) -> ServiceResult<()> { /// Load a task, trying DB first then Markdown. async fn load_task(conn: Option<&Connection>, flow_dir: &Path, task_id: &str) -> Option { if let Some(conn) = conn { - let repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let repo = flowctl_db::TaskRepo::new(conn.clone()); if let Ok(task) = repo.get(task_id).await { return Some(task); } @@ -133,7 +133,7 @@ fn load_task_md(flow_dir: &Path, task_id: &str) -> Option { async fn load_epic(conn: Option<&Connection>, flow_dir: &Path, epic_id: &str) -> Option { if let Some(conn) = conn { - let repo = flowctl_db_lsql::EpicRepo::new(conn.clone()); + let repo = flowctl_db::EpicRepo::new(conn.clone()); if let Ok(epic) = repo.get(epic_id).await { return Some(epic); } @@ -148,7 +148,7 @@ async fn load_epic(conn: Option<&Connection>, flow_dir: &Path, epic_id: &str) -> async fn get_runtime(conn: Option<&Connection>, task_id: &str) -> Option { let conn = conn?; - let repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let repo = flowctl_db::RuntimeRepo::new(conn.clone()); repo.get(task_id).await.ok().flatten() } @@ -161,7 +161,7 @@ async fn load_tasks_for_epic( use std::collections::HashMap; if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); if let Ok(tasks) = task_repo.list_by_epic(epic_id).await { if !tasks.is_empty() { let mut map = HashMap::new(); @@ -286,7 +286,7 @@ async fn propagate_upstream_failure( // Update SQLite if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(tid, Status::UpstreamFailed).await; } @@ -324,10 +324,10 @@ async fn handle_task_failure( let new_retry_count = current_retry_count + 1; if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(task_id, Status::UpForRetry).await; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn.clone()); let rt = RuntimeState { task_id: task_id.to_string(), assignee: runtime.as_ref().and_then(|r| r.assignee.clone()), @@ -358,7 +358,7 @@ async fn handle_task_failure( (Status::UpForRetry, Vec::new()) } else { if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(task_id, Status::Failed).await; } @@ -527,12 +527,12 @@ pub async fn start_task( // Write SQLite (authoritative) if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); task_repo .update_status(&req.task_id, Status::InProgress) .await .map_err(ServiceError::from)?; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn.clone()); runtime_repo .upsert(&runtime_state) .await @@ -757,10 +757,10 @@ pub async fn done_task( // Write SQLite (authoritative) if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(&req.task_id, Status::Done).await; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn.clone()); let now = Utc::now(); let rt = RuntimeState { task_id: req.task_id.clone(), @@ -781,7 +781,7 @@ pub async fn done_task( prs: prs.clone(), ..Evidence::default() }; - let evidence_repo = flowctl_db_lsql::EvidenceRepo::new(conn.clone()); + let evidence_repo = flowctl_db::EvidenceRepo::new(conn.clone()); let _ = evidence_repo.upsert(&req.task_id, &ev).await; } @@ -866,10 +866,10 @@ pub async fn block_task( // Write SQLite (authoritative) if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(&req.task_id, Status::Blocked).await; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn.clone()); let existing = runtime_repo.get(&req.task_id).await.ok().flatten(); let rt = RuntimeState { task_id: req.task_id.clone(), @@ -1063,10 +1063,10 @@ pub async fn restart_task( let mut reset_ids = Vec::new(); for tid in &to_reset { if let Some(conn) = conn { - let task_repo = flowctl_db_lsql::TaskRepo::new(conn.clone()); + let task_repo = flowctl_db::TaskRepo::new(conn.clone()); let _ = task_repo.update_status(tid, Status::Todo).await; - let runtime_repo = flowctl_db_lsql::RuntimeRepo::new(conn.clone()); + let runtime_repo = flowctl_db::RuntimeRepo::new(conn.clone()); let rt = RuntimeState { task_id: tid.clone(), assignee: None,