feat(kebab-store-sqlite): add NotIndexed typed error (fb-27)
New `SqliteStore::open_existing` API + `NotIndexed` signal for the
missing-DB case. kebab-app re-exports the type via its `error_signal`
module so kebab-cli's `error_classify` can map it to
`error.v1 { code: "not_indexed" }`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,4 +12,4 @@ pub use crate::doctor_signal::{DoctorUnhealthy, NoHitSignal, RefusalSignal};
|
||||
|
||||
pub use kebab_llm_local::LlmError;
|
||||
pub use kebab_config::ConfigInvalid; // wired in Task 2
|
||||
// pub use kebab_store_sqlite::NotIndexed; // wired in Task 3
|
||||
pub use kebab_store_sqlite::NotIndexed; // wired in Task 3
|
||||
|
||||
@@ -34,4 +34,4 @@ pub use error::StoreError;
|
||||
pub use eval::{EvalQueryResultRecord, EvalRunRecord, EvalRunRow};
|
||||
pub use fts::rebuild_chunks_fts;
|
||||
pub use jobs::IngestRunRow;
|
||||
pub use store::SqliteStore;
|
||||
pub use store::{NotIndexed, SqliteStore};
|
||||
|
||||
@@ -16,6 +16,18 @@ use rusqlite::{Connection, OptionalExtension, params};
|
||||
use crate::error::StoreError;
|
||||
use crate::schema;
|
||||
|
||||
/// Signal: SQLite database file does not exist, or schema_version does
|
||||
/// not match the binary's expectation.
|
||||
///
|
||||
/// Distinct from generic I/O / SQL errors so kebab-cli can surface
|
||||
/// `code: "not_indexed"` with a hint to run `kebab init` / `kebab ingest`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("not indexed: expected={expected}, found={found:?}")]
|
||||
pub struct NotIndexed {
|
||||
pub expected: String,
|
||||
pub found: Option<String>,
|
||||
}
|
||||
|
||||
/// Monotonic counter used to namespace per-process temp file names so
|
||||
/// concurrent `put_asset_with_bytes` calls in the same millisecond cannot
|
||||
/// collide on `<final>.tmp.<pid>.<n>`.
|
||||
@@ -59,6 +71,41 @@ pub struct SqliteStore {
|
||||
}
|
||||
|
||||
impl SqliteStore {
|
||||
/// Open an existing SQLite DB at the path derived from `config`. Unlike
|
||||
/// `open`, this does NOT create the file — if it is missing, returns a
|
||||
/// [`NotIndexed`] signal suitable for `error.v1` translation.
|
||||
///
|
||||
/// **Does not run migrations** — call [`Self::run_migrations`] next if
|
||||
/// you need the schema initialised.
|
||||
pub fn open_existing(path: &std::path::Path) -> anyhow::Result<Self> {
|
||||
if !path.exists() {
|
||||
return Err(anyhow::Error::new(NotIndexed {
|
||||
expected: path.to_string_lossy().to_string(),
|
||||
found: None,
|
||||
}));
|
||||
}
|
||||
let conn = rusqlite::Connection::open(path)
|
||||
.with_context(|| format!("open sqlite at {}", path.display()))?;
|
||||
apply_pragmas(&conn)?;
|
||||
|
||||
let data_dir = path
|
||||
.parent()
|
||||
.unwrap_or_else(|| std::path::Path::new("."))
|
||||
.to_path_buf();
|
||||
|
||||
tracing::debug!(
|
||||
target: "kebab-store-sqlite",
|
||||
db = %path.display(),
|
||||
"opened existing sqlite store"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
data_dir,
|
||||
copy_threshold_bytes: 0,
|
||||
conn: Mutex::new(conn),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open (or create) the SQLite file under `config.storage.data_dir`,
|
||||
/// apply pragmas (foreign_keys / WAL / synchronous=NORMAL /
|
||||
/// temp_store=MEMORY), and create parent directories as needed.
|
||||
|
||||
19
crates/kebab-store-sqlite/tests/not_indexed.rs
Normal file
19
crates/kebab-store-sqlite/tests/not_indexed.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Signal test: `SqliteStore::open_existing` emits `NotIndexed` when the DB
|
||||
//! file is absent.
|
||||
|
||||
use kebab_store_sqlite::{NotIndexed, SqliteStore};
|
||||
|
||||
#[test]
|
||||
fn not_indexed_signal_emitted_when_db_missing() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let nonexistent_db = dir.path().join("does-not-exist.sqlite");
|
||||
let res = SqliteStore::open_existing(&nonexistent_db);
|
||||
let err = match res {
|
||||
Ok(_) => panic!("opening a missing DB should fail"),
|
||||
Err(e) => e,
|
||||
};
|
||||
let signal = err
|
||||
.downcast_ref::<NotIndexed>()
|
||||
.expect("missing DB error should downcast to NotIndexed");
|
||||
assert_eq!(signal.expected, nonexistent_db.to_string_lossy().as_ref());
|
||||
}
|
||||
Reference in New Issue
Block a user