use super::name_match::{alias_matches, name_matches};
use crate::contracts::availability::SchemaAvailability;
use saya_store::{KnowledgeItem, KnowledgeItemStore, SqliteStateStore};
use saya_types::{ClaimPayload, DatabaseObjectRef, KnowledgeState, ProfileIdentity};
use std::collections::HashMap;
pub(crate) struct Candidate {
pub object: DatabaseObjectRef,
pub items: Vec<KnowledgeItem>,
pub last_seen_unix_ms: i64,
pub tier: u8,
}
pub(crate) struct Selection {
pub candidates: Vec<Candidate>,
pub considered: usize,
pub excluded_by_status: usize,
}
pub(crate) async fn select(
store: &SqliteStateStore,
request: &super::RecallRequest<'_>,
live_schemas: &[(ProfileIdentity, SchemaAvailability)],
) -> Result<Selection, saya_store::KnowledgeStoreError> {
let active: Vec<DatabaseObjectRef> = collect_objects(store, request.profiles).await?;
let considered = active.len();
let items_by_object = collect_items(store, request.profiles).await?;
let mut by_object: Vec<Candidate> = Vec::new();
let mut excluded_by_status = 0usize;
for obj in &active {
let all = items_by_object.get(obj);
let recallable: Vec<KnowledgeItem> = match all {
Some(items) => items
.iter()
.filter(|it| {
request.recall_mode.admits_state(it.state) || admissible_once(it, request)
})
.cloned()
.collect(),
None => Vec::new(),
};
if recallable.is_empty() {
excluded_by_status += 1;
continue;
}
let last_seen_unix_ms = all
.map(|items| items.iter().map(|i| i.updated_unix_ms).max().unwrap_or(0))
.unwrap_or(0);
let tier = best_tier(obj, &recallable, request);
by_object.push(Candidate {
object: obj.clone(),
items: recallable,
last_seen_unix_ms,
tier,
});
}
by_object.sort_by(|a, b| {
a.tier
.cmp(&b.tier)
.then(b.last_seen_unix_ms.cmp(&a.last_seen_unix_ms))
.then(a.object.qualified_name().cmp(&b.object.qualified_name()))
});
let has_query = !request.explicit_refs.is_empty() || !request.terms.is_empty();
if has_query {
by_object.retain(|c| c.tier < 4);
} else {
by_object.clear();
}
let _ = live_schemas;
Ok(Selection {
candidates: by_object,
considered,
excluded_by_status,
})
}
fn admissible_once(item: &saya_store::KnowledgeItem, request: &super::RecallRequest<'_>) -> bool {
request.admit_candidate.as_ref().map(|id| id.as_str()) == Some(&item.id)
&& item.state == KnowledgeState::Pending
}
async fn collect_objects(
store: &SqliteStateStore,
profiles: &[ProfileIdentity],
) -> Result<Vec<DatabaseObjectRef>, saya_store::KnowledgeStoreError> {
let mut out = Vec::new();
for profile in profiles {
out.extend(store.objects_for_profile(profile).await?);
}
Ok(out)
}
async fn collect_items(
store: &SqliteStateStore,
profiles: &[ProfileIdentity],
) -> Result<
HashMap<DatabaseObjectRef, Vec<saya_store::KnowledgeItem>>,
saya_store::KnowledgeStoreError,
> {
let mut by_object: HashMap<DatabaseObjectRef, Vec<saya_store::KnowledgeItem>> = HashMap::new();
for profile in profiles {
for item in store.knowledge_for_profile(profile).await? {
by_object.entry(item.object.clone()).or_default().push(item);
}
}
Ok(by_object)
}
fn best_tier(
object: &DatabaseObjectRef,
items: &[KnowledgeItem],
request: &super::RecallRequest<'_>,
) -> u8 {
if request.explicit_refs.iter().any(|r| r == object) {
return 1;
}
let aliases: Vec<String> = items
.iter()
.filter_map(|it| match &it.value {
ClaimPayload::TableAlias { alias, .. } => Some(alias.trim().to_lowercase()),
_ => None,
})
.collect();
let term_lc: Vec<String> = request
.terms
.iter()
.map(|t| t.trim().to_lowercase())
.collect();
if aliases.iter().any(|a| alias_matches(a, &term_lc)) {
return 2;
}
let name_segment = object.object().to_lowercase();
let desc_text: Vec<String> = items
.iter()
.filter_map(|it| match &it.value {
ClaimPayload::TableDescription { text, .. } => Some(text.to_lowercase()),
ClaimPayload::TableGrain { description, .. } => Some(description.to_lowercase()),
ClaimPayload::ColumnDescription { text, .. } => Some(text.to_lowercase()),
_ => None,
})
.collect();
if term_lc
.iter()
.any(|t| name_matches(&name_segment, t) || desc_text.iter().any(|d| d.contains(t)))
{
return 3;
}
4
}