use crate::db::schema::CozoDb;
const CREATE_EMBEDDING_STATE: &str = r#":create embedding_state {qualified_name: String => usearch_key: Int, content_hash: String, state: String, embedded_at: String}"#;
const CREATE_QN_INDEX: &str = r#"::index create embedding_state:qn_index { qualified_name }"#;
const CREATE_KEY_INDEX: &str =
r#"::index create embedding_state:usearch_key_index { usearch_key }"#;
const CREATE_STATE_INDEX: &str = r#"::index create embedding_state:state_index { state }"#;
#[derive(Debug, Clone)]
pub struct EmbeddingStateRow {
pub qualified_name: String,
pub usearch_key: i64,
pub content_hash: String,
pub state: String,
pub embedded_at: String,
}
pub fn ensure_embedding_state_table(db: &CozoDb) -> Result<(), Box<dyn std::error::Error>> {
let existing: std::collections::HashSet<String> =
crate::db::schema::run_script(db, "::relations", Default::default())
.map(|r| {
r.rows
.iter()
.filter_map(|row| row.first().and_then(|v| v.get_str().map(String::from)))
.collect()
})
.unwrap_or_default();
if !existing.contains("embedding_state") {
crate::db::schema::run_script(db, CREATE_EMBEDDING_STATE, Default::default())?;
for idx in &[CREATE_QN_INDEX, CREATE_KEY_INDEX, CREATE_STATE_INDEX] {
if let Err(e) = crate::db::schema::run_script(db, idx, Default::default()) {
tracing::debug!("embedding_state index note: {:?}", e);
}
}
tracing::info!("created embedding_state table");
}
if !existing.contains("embedding_vectors") {
crate::db::schema::run_script(db, CREATE_EMBEDDING_VECTORS, Default::default())?;
tracing::info!("created embedding_vectors relation");
}
if !existing.contains("embedding_vectors:vec_idx") {
let hnsw_create = build_hnsw_create_stmt();
match crate::db::schema::run_script(db, &hnsw_create, Default::default()) {
Ok(_) => tracing::info!("created HNSW index embedding_vectors:vec_idx"),
Err(e) => tracing::warn!(
"failed to create HNSW index on embedding_vectors (query len={}): {:?}",
hnsw_create.len(),
e
),
}
}
Ok(())
}
const CREATE_EMBEDDING_VECTORS: &str =
r#":create embedding_vectors {qualified_name: String => vector: <F32; 384>}"#;
fn build_hnsw_create_stmt() -> String {
let m = std::env::var("LEANKG_HNSW_M")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| (4..=256).contains(v))
.unwrap_or(50);
let ef_construction = std::env::var("LEANKG_HNSW_EF_CONST")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|v| (1..=2000).contains(v))
.unwrap_or(20);
format!(
r#"::hnsw create embedding_vectors:vec_idx {{
dim: 384,
dtype: F32,
fields: [vector],
distance: Cosine,
ef_construction: {ef_construction},
m: {m},
extend_candidates: false,
keep_pruned_connections: false
}}"#
)
}
pub fn mark_stale_for_qualified_names(
db: &CozoDb,
qualified_names: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
if qualified_names.is_empty() {
return Ok(());
}
let now = now_iso();
for chunk in qualified_names.chunks(UPSERT_CHUNK) {
let rows: Vec<String> = chunk
.iter()
.map(|qn| {
let key_i64: i64 = 0;
format!(
"[{}, {}, {}, {}, {}]",
serde_json::Value::String(qn.clone()),
serde_json::Value::Number(key_i64.into()),
serde_json::Value::String("".to_string()),
serde_json::Value::String("stale".to_string()),
serde_json::Value::String(now.clone()),
)
})
.collect();
let values_clause = rows.join(", ");
let query = format!(
r#"?[qualified_name, usearch_key, content_hash, state, embedded_at] <- [{values_clause}]
:put embedding_state {{qualified_name, usearch_key, content_hash, state, embedded_at}}"#
);
crate::db::schema::run_script(db, &query, Default::default())?;
}
Ok(())
}
pub fn list_stale(db: &CozoDb) -> Result<Vec<EmbeddingStateRow>, Box<dyn std::error::Error>> {
let query = r#"?[qualified_name, usearch_key, content_hash, state, embedded_at] := *embedding_state[qualified_name, usearch_key, content_hash, state, embedded_at], state != "fresh""#;
let result = crate::db::schema::run_script(db, query, Default::default())?;
Ok(result.rows.iter().filter_map(row_to_state_row).collect())
}
pub fn list_orphans(db: &CozoDb) -> Result<Vec<EmbeddingStateRow>, Box<dyn std::error::Error>> {
let query = r#"
?[qualified_name, usearch_key, content_hash, state, embedded_at] :=
*embedding_state[qualified_name, usearch_key, content_hash, state, embedded_at],
not *code_elements[qualified_name, _, _, _, _, _, _, _, _, _, _, _, _]
"#;
let result = crate::db::schema::run_script(db, query, Default::default())?;
Ok(result.rows.iter().filter_map(row_to_state_row).collect())
}
pub fn list_all(db: &CozoDb) -> Result<Vec<EmbeddingStateRow>, Box<dyn std::error::Error>> {
let query = r#"?[qualified_name, usearch_key, content_hash, state, embedded_at] := *embedding_state[qualified_name, usearch_key, content_hash, state, embedded_at]"#;
let result = crate::db::schema::run_script(db, query, Default::default())?;
Ok(result.rows.iter().filter_map(row_to_state_row).collect())
}
const UPSERT_CHUNK: usize = 500;
pub fn upsert_fresh(db: &CozoDb, updates: &[FreshRow]) -> Result<(), Box<dyn std::error::Error>> {
if updates.is_empty() {
return Ok(());
}
let now = now_iso();
for chunk in updates.chunks(UPSERT_CHUNK) {
let rows: Vec<String> = chunk
.iter()
.map(|u| {
let key_i64 = u.usearch_key as i64;
format!(
"[{}, {}, {}, {}, {}]",
serde_json::Value::String(u.qualified_name.clone()),
serde_json::Value::Number(key_i64.into()),
serde_json::Value::String(u.content_hash.clone()),
serde_json::Value::String("fresh".to_string()),
serde_json::Value::String(now.clone()),
)
})
.collect();
let values_clause = rows.join(", ");
let query = format!(
r#"?[qualified_name, usearch_key, content_hash, state, embedded_at] <- [{values_clause}]
:put embedding_state {{qualified_name, usearch_key, content_hash, state, embedded_at}}"#
);
crate::db::schema::run_script(db, &query, Default::default())?;
}
Ok(())
}
pub fn delete_state_rows(
db: &CozoDb,
rows: &[EmbeddingStateRow],
) -> Result<(), Box<dyn std::error::Error>> {
if rows.is_empty() {
return Ok(());
}
for chunk in rows.chunks(UPSERT_CHUNK) {
let literals: Vec<String> = chunk
.iter()
.map(|r| format!("[{}]", serde_json::Value::String(r.qualified_name.clone())))
.collect();
let values_clause = literals.join(", ");
let query = format!(
r#"?[qualified_name] <- [{values_clause}] :rm embedding_state {{qualified_name}}"#
);
crate::db::schema::run_script(db, &query, Default::default())?;
}
Ok(())
}
pub fn count_by_state(db: &CozoDb) -> Result<StateCounts, Box<dyn std::error::Error>> {
let all = list_all(db)?;
let mut counts = StateCounts::default();
for row in all {
match row.state.as_str() {
"fresh" => counts.fresh += 1,
"stale" => counts.stale += 1,
_ => counts.other += 1,
}
}
Ok(counts)
}
#[derive(Debug, Clone, Default)]
pub struct StateCounts {
pub fresh: usize,
pub stale: usize,
pub other: usize,
}
#[derive(Debug, Clone)]
pub struct FreshRow {
pub qualified_name: String,
pub usearch_key: u64,
pub content_hash: String,
}
fn row_to_state_row(row: &Vec<cozo::DataValue>) -> Option<EmbeddingStateRow> {
let qualified_name = row.first()?.get_str()?.to_string();
let usearch_key = row.get(1)?.get_int()?;
let content_hash = row.get(2)?.get_str()?.to_string();
let state = row.get(3)?.get_str()?.to_string();
let embedded_at = row.get(4)?.get_str()?.to_string();
Some(EmbeddingStateRow {
qualified_name,
usearch_key,
content_hash,
state,
embedded_at,
})
}
fn now_iso() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{}", secs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_iso_returns_numeric_string() {
let ts = now_iso();
assert!(
ts.chars().all(|c| c.is_ascii_digit()),
"now_iso must be numeric: {ts}"
);
assert!(!ts.is_empty());
}
#[test]
fn state_counts_default_is_all_zero() {
let counts = StateCounts::default();
assert_eq!(counts.fresh, 0);
assert_eq!(counts.stale, 0);
assert_eq!(counts.other, 0);
}
#[test]
fn fresh_row_fields_are_accessible() {
let row = FreshRow {
qualified_name: "src/main.rs::main".to_string(),
usearch_key: 42,
content_hash: "abc123".to_string(),
};
assert_eq!(row.qualified_name, "src/main.rs::main");
assert_eq!(row.usearch_key, 42);
assert_eq!(row.content_hash, "abc123");
}
#[test]
fn embedding_state_row_fields_are_accessible() {
let row = EmbeddingStateRow {
qualified_name: "q".to_string(),
usearch_key: 7,
content_hash: "h".to_string(),
state: "fresh".to_string(),
embedded_at: "12345".to_string(),
};
assert_eq!(row.qualified_name, "q");
assert_eq!(row.usearch_key, 7);
assert_eq!(row.content_hash, "h");
assert_eq!(row.state, "fresh");
assert_eq!(row.embedded_at, "12345");
}
#[test]
fn row_to_state_row_parses_valid_row() {
let row = vec![
cozo::DataValue::Str("qn".into()),
cozo::DataValue::Num(cozo::Num::Int(5)),
cozo::DataValue::Str("hash".into()),
cozo::DataValue::Str("stale".into()),
cozo::DataValue::Str("999".into()),
];
let parsed = row_to_state_row(&row).expect("should parse");
assert_eq!(parsed.qualified_name, "qn");
assert_eq!(parsed.usearch_key, 5);
assert_eq!(parsed.content_hash, "hash");
assert_eq!(parsed.state, "stale");
assert_eq!(parsed.embedded_at, "999");
}
#[test]
fn row_to_state_row_returns_none_for_empty_row() {
let row: Vec<cozo::DataValue> = vec![];
assert!(row_to_state_row(&row).is_none());
}
#[test]
fn row_to_state_row_returns_none_for_short_row() {
let row = vec![
cozo::DataValue::Str("qn".into()),
cozo::DataValue::Num(cozo::Num::Int(5)),
];
assert!(row_to_state_row(&row).is_none());
}
}