use crate::error::{DbError, Result};
use crate::schema::ddl;
use crate::vector::ModelName;
#[doc(hidden)]
pub async fn register_model(
conn: &libsql::Connection,
model: &ModelName,
dim: usize,
) -> Result<()> {
if dim == 0 {
return Err(DbError::DimMismatch {
got: 0,
expected: 1,
model: model.to_string(),
});
}
if let Some(existing) = read_declared_dimension(conn, model).await? {
if existing != dim {
return Err(DbError::DimMismatch {
got: dim,
expected: existing,
model: model.to_string(),
});
}
return Ok(());
}
let tx = conn
.transaction_with_behavior(libsql::TransactionBehavior::Immediate)
.await?;
let res: Result<()> = async {
tx.execute(&ddl::create_embeddings_table(model, dim), ())
.await?;
tx.execute(&ddl::create_embeddings_index(model), ()).await?;
Ok(())
}
.await;
match res {
Ok(()) => {
tx.commit().await?;
Ok(())
}
Err(e) => {
let _ = tx.rollback().await;
Err(e)
}
}
}
pub async fn declared_dimension(conn: &libsql::Connection, model: &ModelName) -> Result<usize> {
read_declared_dimension(conn, model)
.await?
.ok_or_else(|| DbError::ModelNotRegistered {
model: model.to_string(),
table: model.table(),
})
}
async fn read_declared_dimension(
conn: &libsql::Connection,
model: &ModelName,
) -> Result<Option<usize>> {
let mut rows = conn
.query(&format!("PRAGMA table_info({})", model.table()), ())
.await?;
while let Some(row) = rows.next().await? {
let name: String = row.get(1)?;
if name == "embedding" {
let declared: String = row.get(2)?;
return parse_f32_blob_dim(&declared).map(Some).ok_or_else(|| {
DbError::ModelNotRegistered {
model: model.to_string(),
table: format!(
"{} (column `embedding` is declared {declared:?}, not F32_BLOB(n))",
model.table()
),
}
});
}
}
Ok(None)
}
fn parse_f32_blob_dim(declared: &str) -> Option<usize> {
let t = declared.trim();
if !t.get(..9)?.eq_ignore_ascii_case("F32_BLOB(") || !t.ends_with(')') {
return None;
}
t[9..t.len() - 1].trim().parse::<usize>().ok()
}
pub async fn registered_models(conn: &libsql::Connection) -> Result<Vec<ModelName>> {
let mut rows = conn
.query(
"SELECT name FROM sqlite_master
WHERE type = 'table'
AND name LIKE 'embeddings_%'
AND name NOT LIKE '%_shadow'
ORDER BY name",
(),
)
.await?;
let mut out = Vec::new();
while let Some(row) = rows.next().await? {
let table: String = row.get(0)?;
if let Some(suffix) = table.strip_prefix("embeddings_") {
if let Ok(model) = ModelName::new(suffix) {
out.push(model);
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::parse_f32_blob_dim;
#[test]
fn reads_the_dimension_out_of_a_declared_type() {
assert_eq!(parse_f32_blob_dim("F32_BLOB(768)"), Some(768));
assert_eq!(parse_f32_blob_dim("f32_blob(1536)"), Some(1536));
assert_eq!(parse_f32_blob_dim(" F32_BLOB( 4 ) "), Some(4));
}
#[test]
fn refuses_to_invent_a_dimension() {
for bad in [
"BLOB",
"TEXT",
"F32_BLOB",
"F32_BLOB()",
"F32_BLOB(x)",
"F32_BLOB(4",
"",
] {
assert_eq!(parse_f32_blob_dim(bad), None, "{bad:?} yielded a dimension");
}
}
}