refactor: replace LanceDB Database decorator with VectorStore composition

Instead of wrapping all ~80 Database trait methods in a 664-line decorator
(lancedb_wrapper.rs), introduce a 4-method VectorStore trait that any vector
backend can implement. Workspace composes FTS from the database with vector
search from the external store via RRF fusion.

- Add src/workspace/vector_store.rs with VectorStore trait
- Rewrite lancedb_store.rs to implement VectorStore (not wrap Database)
- Delete src/db/lancedb_wrapper.rs (664 lines removed)
- Remove get_chunk_by_id from Database trait and all backends
- Workspace gains with_vector_store() builder for optional composition
- Fix LanceDB tests: bypass_vector_index() for brute-force search
- Fix integration tests: use temp file DB (libSQL :memory: is per-connection)
- Merge duplicate mod tests in config.rs

Net: -724 lines. Adding a new vector backend requires 4 methods, not 80.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
2026-03-08 00:06:04 -08:00
co-authored by Claude Opus 4.6
parent c9dbcb2627
commit 1e0494e72d
13 changed files with 445 additions and 1101 deletions
+12 -12
View File
@@ -378,7 +378,7 @@ async fn main() -> anyhow::Result<()> {
#[cfg(feature = "libsql")]
let mut libsql_db: Option<std::sync::Arc<libsql::Database>> = None;
let mut inner_db: Option<Arc<dyn ironclaw::db::Database>> = if cli.no_db {
let db: Option<Arc<dyn ironclaw::db::Database>> = if cli.no_db {
tracing::warn!("Running without database connection");
None
} else {
@@ -435,8 +435,8 @@ async fn main() -> anyhow::Result<()> {
}
};
// Optionally wrap with LanceDB vector store for workspace semantic search
let db: Option<Arc<dyn ironclaw::db::Database>> = if let Some(inner) = inner_db.take() {
// Create optional external vector store for workspace semantic search
let vector_store: Option<Arc<dyn ironclaw::workspace::VectorStore>> = {
#[cfg(feature = "lancedb")]
{
if config.database.vector_backend == ironclaw::config::VectorBackend::LanceDb {
@@ -449,12 +449,9 @@ async fn main() -> anyhow::Result<()> {
.await
.map_err(|e| anyhow::anyhow!("LanceDB: {}", e))?;
tracing::info!("LanceDB vector store connected for workspace search");
Some(Arc::new(ironclaw::db::lancedb_wrapper::DbWithLanceVectorStore::new(
inner,
Arc::new(store),
)) as Arc<dyn ironclaw::db::Database>)
Some(Arc::new(store) as Arc<dyn ironclaw::workspace::VectorStore>)
} else {
Some(inner)
None
}
}
#[cfg(not(feature = "lancedb"))]
@@ -463,12 +460,9 @@ async fn main() -> anyhow::Result<()> {
anyhow::bail!(
"VECTOR_BACKEND=lancedb requires the 'lancedb' feature. Build with: cargo build --features lancedb"
);
} else {
Some(inner)
}
None
}
} else {
None
};
// Post-init operations using the database
@@ -761,6 +755,9 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref emb) = embeddings {
workspace = workspace.with_embeddings(emb.clone());
}
if let Some(ref vs) = vector_store {
workspace = workspace.with_vector_store(vs.clone());
}
let workspace = Arc::new(workspace);
tools.register_memory_tools(workspace);
}
@@ -1265,6 +1262,9 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref emb) = embeddings {
ws = ws.with_embeddings(emb.clone());
}
if let Some(ref vs) = vector_store {
ws = ws.with_vector_store(vs.clone());
}
Some(Arc::new(ws))
} else {
None