use crate::embedding::{EMBED_MODEL_ID, EMBED_MODEL_REVISION};
use crate::embedding_profiles::profile_for;
use rusqlite::{Connection, OptionalExtension};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelIdentity {
pub model_id: String,
pub revision: String,
}
impl ModelIdentity {
pub fn default_identity() -> Self {
Self {
model_id: EMBED_MODEL_ID.to_string(),
revision: EMBED_MODEL_REVISION.to_string(),
}
}
pub fn display(&self) -> String {
format!("{}@{}", self.model_id, self.revision)
}
}
type IdentityRow = Option<(Option<String>, Option<String>, Option<String>)>;
fn read_identity_row(conn: &Connection) -> crate::sqlite::Result<IdentityRow> {
conn.query_row(
"SELECT model_id, model_revision, migration_marker FROM model_identity WHERE id = 1",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
)
.optional()
.map_err(crate::sqlite::Error::from)
}
pub fn read_identity(conn: &Connection) -> crate::sqlite::Result<Option<ModelIdentity>> {
let Some(row) = read_identity_row(conn)? else {
return Ok(None);
};
let (model_id, revision, _marker) = row;
match model_id.filter(|id| !id.is_empty()) {
Some(model_id) => Ok(Some(ModelIdentity {
model_id,
revision: revision.unwrap_or_default(),
})),
None => Ok(None),
}
}
pub fn read_marker(conn: &Connection) -> crate::sqlite::Result<Option<String>> {
Ok(read_identity_row(conn)?.and_then(|row| row.2))
}
pub fn is_migrating(conn: &Connection) -> crate::sqlite::Result<bool> {
Ok(read_marker(conn)?.is_some())
}
pub fn current_identity(conn: &Connection) -> crate::sqlite::Result<ModelIdentity> {
Ok(read_identity(conn)?.unwrap_or_else(ModelIdentity::default_identity))
}
pub fn configured_identity(configured_model_id: &str) -> ModelIdentity {
match profile_for(configured_model_id) {
Ok(profile) => ModelIdentity {
model_id: profile.model_id.to_string(),
revision: profile.revision.to_string(),
},
Err(_) => ModelIdentity {
model_id: configured_model_id.to_string(),
revision: String::new(),
},
}
}
pub fn read_identity_and_marker(
conn: &Connection,
) -> crate::sqlite::Result<(Option<ModelIdentity>, Option<String>)> {
let Some(row) = read_identity_row(conn)? else {
return Ok((None, None));
};
let (model_id, revision, marker) = row;
let identity = model_id
.filter(|id| !id.is_empty())
.map(|model_id| ModelIdentity {
model_id,
revision: revision.unwrap_or_default(),
});
Ok((identity, marker))
}
pub fn assert_identity_ok(
conn: &Connection,
configured_model_id: &str,
) -> Result<(), crate::errors::Error> {
let (recorded, marker) = read_identity_and_marker(conn).map_err(crate::errors::Error::from)?;
if let Some(marker) = marker {
return Err(crate::errors::Error::Config(format!(
"model migration in progress: database is migrating to {marker} — add/update/search are refused until the migration completes. Run `vipune reindex --force` to complete it."
)));
}
let effective = recorded.unwrap_or_else(ModelIdentity::default_identity);
let configured = configured_identity(configured_model_id);
if effective != configured {
return Err(crate::errors::Error::Config(format!(
"model identity mismatch: database was last embedded with {} but the configured model is {}. Re-embed the store with `vipune reindex --force`.",
effective.display(),
configured.display()
)));
}
Ok(())
}
#[cfg(test)]
#[path = "identity_tests.rs"]
mod identity_tests;