use crate::embedding::EmbeddingEngine;
use crate::embedding::MAX_EMBEDDING_TOKENS;
use crate::sqlite::Database;
use crate::sqlite::Error;
use crate::sqlite::embedding::classify_embedding;
use crate::sqlite::identity::ModelIdentity;
use rusqlite::Transaction;
fn update_embedding_in(tx: &Transaction<'_>, id: &str, embedding: &[f32]) -> Result<(), Error> {
let blob = crate::sqlite::vec_to_blob(embedding)?;
tx.execute(
"UPDATE memories SET embedding = ?1 WHERE id = ?2",
rusqlite::params![&blob, id],
)
.map_err(|e| Error::Sqlite(e.to_string()))?;
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub struct OverLimitRow {
pub id: String,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ReembedFailure {
pub id: String,
pub error: String,
}
pub fn over_limit_row_ids(
db: &Database,
engine: &EmbeddingEngine,
projects: &[String],
passage_prefix: &str,
) -> Result<Vec<OverLimitRow>, Error> {
let mut offending: Vec<OverLimitRow> = Vec::new();
for project_id in projects {
let rows = db.list_all_rows_for_project(project_id)?;
for (id, content, _embedding) in rows {
let prefixed = format!("{passage_prefix}{content}");
let count = engine
.token_count(&prefixed)
.map_err(|e| Error::InvalidInput(format!("token count failed: {e}")))?;
if count > MAX_EMBEDDING_TOKENS {
offending.push(OverLimitRow { id });
}
}
}
Ok(offending)
}
pub(crate) fn force_reembed_project<F>(
db: &mut Database,
project_id: &str,
mut embed: F,
) -> Result<(usize, usize, Vec<ReembedFailure>), Error>
where
F: FnMut(&str) -> Result<Vec<f32>, Error>,
{
let rows = db.list_all_rows_for_project(project_id)?;
let mut reindexed: usize = 0;
let mut skipped: usize = 0;
let mut failed: Vec<ReembedFailure> = Vec::new();
let tx = db.begin_transaction()?;
for (id, content, embedding) in rows {
if classify_embedding(&embedding) == crate::sqlite::embedding::EmbeddingClass::Unknown {
skipped += 1;
continue;
}
match embed(&content) {
Ok(new_vec) => {
update_embedding_in(&tx, &id, &new_vec)?;
reindexed += 1;
}
Err(e) => {
failed.push(ReembedFailure {
id,
error: e.to_string(),
});
}
}
}
commit_tx(tx)?;
Ok((reindexed, skipped, failed))
}
pub fn force_migrate_database<F>(
db: &mut Database,
target: &ModelIdentity,
projects: &[String],
mut embed: F,
) -> Result<(usize, usize, Vec<ReembedFailure>), Error>
where
F: FnMut(&str) -> Result<Vec<f32>, Error>,
{
write_marker(db, target)?;
let mut totals: (usize, usize, Vec<ReembedFailure>) = (0, 0, Vec::new());
for project_id in projects {
let (reindexed, skipped, failed) = force_reembed_project(db, project_id, &mut embed)?;
totals.0 += reindexed;
totals.1 += skipped;
totals.2.extend(failed);
}
if !totals.2.is_empty() {
return Err(Error::InvalidInput(format!(
"force reindex failed on {} row(s) across {} project(s): the migration marker is left in place and the new model identity was not recorded. Fix the errors and re-run `vipune reindex --force`.",
totals.2.len(),
projects.len()
)));
}
record_identity_and_clear_marker(db, target)?;
Ok(totals)
}
pub(crate) fn record_identity_and_clear_marker(
db: &mut Database,
identity: &ModelIdentity,
) -> Result<(), Error> {
let tx = db.begin_transaction()?;
tx.execute(
"INSERT INTO model_identity (id, model_id, model_revision, migration_marker)
VALUES (1, ?1, ?2, NULL)
ON CONFLICT(id) DO UPDATE SET model_id = excluded.model_id,
model_revision = excluded.model_revision,
migration_marker = NULL",
(&identity.model_id, &identity.revision),
)
.map_err(|e| Error::Sqlite(e.to_string()))?;
commit_tx(tx).map_err(|e| Error::Sqlite(e.to_string()))?;
Ok(())
}
pub(crate) fn write_marker(db: &Database, target: &ModelIdentity) -> Result<(), Error> {
let marker = migration_marker_for(target);
db.conn()
.execute(
"INSERT INTO model_identity (id, model_id, model_revision, migration_marker)
VALUES (1, NULL, NULL, ?1)
ON CONFLICT(id) DO UPDATE SET migration_marker = excluded.migration_marker",
[marker],
)
.map_err(|e| Error::Sqlite(e.to_string()))?;
Ok(())
}
pub(crate) fn migration_marker_for(identity: &ModelIdentity) -> String {
format!("migrating to {}", identity.display())
}
fn commit_tx(tx: Transaction<'_>) -> rusqlite::Result<()> {
tx.commit()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn migration_marker_names_target() {
let target = ModelIdentity {
model_id: "m".to_string(),
revision: "r".to_string(),
};
assert_eq!(migration_marker_for(&target), "migrating to m@r");
}
}