use uqa_sql::ast::ColumnType;
use uqa_storage::{
vector_index::{DiskANNIndexParams, HNSWIndexParams, IVFIndexParams, VectorIndexSpec},
CatalogIndexRow, DocumentStore, StorageBackendError, StorageBackendResult, VectorIndex,
};
pub fn is_vector_method(method: &str) -> bool {
["ivf", "hnsw", "diskann"]
.iter()
.any(|candidate| method.eq_ignore_ascii_case(candidate))
}
pub fn stored_spec(
row: &CatalogIndexRow,
dimensions: u32,
) -> StorageBackendResult<VectorIndexSpec> {
let parameters = serde_json::from_str(&row.parameters_json)?;
match row.index_type.to_ascii_lowercase().as_str() {
"ivf" => IVFIndexParams::from_catalog_map(¶meters).map(VectorIndexSpec::IVF),
"hnsw" => HNSWIndexParams::from_catalog_map(¶meters).map(VectorIndexSpec::HNSW),
"diskann" => DiskANNIndexParams::from_catalog_map(dimensions, ¶meters)
.map(VectorIndexSpec::DiskANN),
_ => Err(StorageBackendError::Other(
"catalog row is not a physical vector index".into(),
)),
}
}
pub fn field_index<'a>(
rows: impl Iterator<Item = &'a CatalogIndexRow>,
table: &str,
field: &str,
) -> StorageBackendResult<Option<&'a CatalogIndexRow>> {
let mut found = None;
for row in rows {
if row.table_name == table
&& is_vector_method(&row.index_type)
&& super::index_references_column(row, field)?
&& found.replace(row).is_some()
{
return Err(StorageBackendError::Other(format!(
"multiple physical vector indexes target `{table}`.`{field}`"
)));
}
}
Ok(found)
}
pub struct ColumnVectorRename {
pub dimensions: u32,
pub retained: Option<Box<dyn VectorIndex>>,
}
pub fn detach_for_column_rename(
indexes: &mut uqa_storage::vector_index::VectorIndexes,
field: &str,
) -> StorageBackendResult<Option<ColumnVectorRename>> {
let Some(mut index) = indexes.live_mut()?.remove(field) else {
return Ok(None);
};
let dimensions = index.dimensions();
let retained = if index.index_kind() == "diskann" {
Some(index)
} else {
index.clear()?;
None
};
Ok(Some(ColumnVectorRename {
dimensions,
retained,
}))
}
pub fn prepare_column_rewrite(
indexes: &mut uqa_storage::vector_index::VectorIndexes,
field: &str,
mut canonical: Box<dyn VectorIndex>,
) -> StorageBackendResult<()> {
let indexes = indexes.live_mut()?;
if let Some(mut old) = indexes.remove(field) {
if old.index_kind() != "diskann" {
old.clear()?;
}
}
canonical.clear()?;
indexes.insert(field.into(), canonical);
Ok(())
}
pub fn remove_for_column_conversion(
context: &crate::schema::indexes::registry::IndexRegistryContext<'_>,
table: &str,
column: &str,
clear: impl FnOnce() -> StorageBackendResult<()>,
) -> StorageBackendResult<()> {
use crate::schema::indexes::registry::{binding, lifecycle};
let catalog = context.identities.catalog.current_catalog_snapshot();
let mut rows = Vec::new();
for row in catalog.catalog_indexes() {
if row.table_name == table
&& is_vector_method(&row.index_type)
&& super::index_references_column(row, column)?
{
rows.push(row.relation.clone());
}
}
for relation in &rows {
binding::removal(context, relation)?;
}
clear()?;
for relation in rows {
lifecycle::remove(context, &relation, false)?;
}
Ok(())
}
pub fn populate(
index: &mut dyn VectorIndex,
documents: &dyn DocumentStore,
field: &str,
ty: Option<&ColumnType>,
) -> StorageBackendResult<()> {
for (document, values) in documents.iter_all()? {
let Some(value) = values
.get(field)
.filter(|value| !matches!(value, uqa_core::Value::Null))
else {
continue;
};
let vectors = match ty {
Some(ty @ (ColumnType::Vector(_) | ColumnType::Tensor(_))) => {
uqa_sql::assignment::vectors::index_vectors_for_type(value, ty)
}
_ => uqa_sql::expr::value_to_vector(value).map(|vector| vec![vector]),
}
.map_err(|error| StorageBackendError::backend("vector index backfill", error))?;
index.add_many(document, vectors)?;
}
index.initialize()
}