use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use anyhow::Result;
use super::Signature;
use crate::catalog::Index;
use crate::exec::physical_expr::EvalContext;
use crate::exec::{BoxFut, ContextLevel, SendSyncRequirement};
use crate::expr::Kind;
use crate::expr::idiom::Idiom;
use crate::idx::IndexKeyBase;
use crate::idx::ft::MatchRef;
use crate::idx::ft::fulltext::{FullTextIndex, QueryTerms, Scorer};
use crate::kvs::index::filter_online_indexes;
use crate::val::{Number, RecordId, TableName, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexContextKind {
FullText,
Knn,
}
#[derive(Clone)]
pub enum IndexContext {
FullText(Arc<MatchContext>),
Knn(Arc<KnnContext>),
}
impl Debug for IndexContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::FullText(ctx) => f.debug_tuple("IndexContext::FullText").field(ctx).finish(),
Self::Knn(ctx) => f.debug_tuple("IndexContext::Knn").field(ctx).finish(),
}
}
}
pub trait IndexFunction: SendSyncRequirement + Debug {
fn name(&self) -> &'static str;
#[allow(unused)]
fn signature(&self) -> Signature;
#[allow(unused)]
fn return_type(&self, _arg_types: &[Kind]) -> Result<Kind> {
Ok(self.signature().returns)
}
fn index_context_kind(&self) -> IndexContextKind;
fn index_ref_arg_index(&self) -> Option<usize>;
fn required_context(&self) -> ContextLevel {
ContextLevel::Root
}
fn invoke_async<'a>(
&'a self,
ctx: &'a EvalContext<'_>,
index_ctx: &'a IndexContext,
args: Vec<Value>,
) -> BoxFut<'a, Result<Value>>;
}
pub struct MatchContext {
pub idiom: Idiom,
pub query: String,
pub table: TableName,
ft_cache: tokio::sync::OnceCell<(FullTextIndex, QueryTerms, Option<Scorer>)>,
}
impl MatchContext {
pub fn new(idiom: Idiom, query: String, table: TableName) -> Self {
Self {
idiom,
query,
table,
ft_cache: tokio::sync::OnceCell::new(),
}
}
pub async fn ft_resources(
&self,
ctx: &EvalContext<'_>,
) -> Result<&(FullTextIndex, QueryTerms, Option<Scorer>)> {
self.ft_cache
.get_or_try_init(|| async {
use crate::catalog::providers::TableProvider;
let frozen = ctx.exec_ctx.ctx();
let root = ctx.exec_ctx.root();
let opt = root
.options
.as_ref()
.ok_or_else(|| anyhow::anyhow!("IndexFunction requires Options context"))?;
let tx = ctx.txn();
let db_ctx = ctx.exec_ctx.database().map_err(|e| {
anyhow::anyhow!("IndexFunction requires database context: {}", e)
})?;
let ns_id = db_ctx.ns_ctx.ns.namespace_id;
let db_id = db_ctx.db.database_id;
let indexes = tx
.all_tb_indexes(ns_id, db_id, &self.table, ctx.exec_ctx.version_stamp())
.await?;
let indexes = if ctx.exec_ctx.version_stamp().is_none() {
filter_online_indexes(tx.as_ref(), ns_id, db_id, indexes).await?
} else {
indexes
};
let index_def = indexes
.iter()
.find(|idx| {
matches!(&idx.index, Index::FullText(_))
&& idx.cols.iter().any(|col| col.0 == self.idiom.0)
})
.ok_or_else(|| {
anyhow::anyhow!(
"No full-text index found for field {:?} on table {}",
self.idiom,
self.table
)
})?;
let ft_params = match &index_def.index {
Index::FullText(params) => params,
_ => unreachable!("Already checked for FullText above"),
};
let ikb = IndexKeyBase::new(ns_id, db_id, self.table.clone(), index_def.index_id);
let fti = FullTextIndex::new(
frozen.get_index_stores(),
tx.as_ref(),
ikb,
ft_params,
&frozen.config.file_allowlist,
)
.await?;
let query_terms = {
let mut stack = reblessive::TreeStack::new();
stack
.enter(|stk| {
fti.extract_querying_terms(stk, frozen, opt, self.query.clone())
})
.finish()
.await?
};
let scorer = fti.new_scorer(frozen).await?;
Ok((fti, query_terms, scorer))
})
.await
}
}
impl Debug for MatchContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MatchContext")
.field("idiom", &self.idiom)
.field("query", &self.query)
.field("table", &self.table)
.field("initialized", &self.ft_cache.initialized())
.finish()
}
}
pub struct KnnContext {
distances: tokio::sync::RwLock<HashMap<RecordId, Number>>,
}
impl KnnContext {
pub fn new() -> Self {
Self {
distances: tokio::sync::RwLock::new(HashMap::new()),
}
}
pub async fn insert(&self, rid: RecordId, dist: Number) {
self.distances.write().await.insert(rid, dist);
}
pub async fn get(&self, rid: &RecordId) -> Option<Number> {
self.distances.read().await.get(rid).copied()
}
}
impl Default for KnnContext {
fn default() -> Self {
Self::new()
}
}
impl Debug for KnnContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.distances.try_read() {
Ok(guard) => f.debug_struct("KnnContext").field("entries", &guard.len()).finish(),
Err(_) => f.debug_struct("KnnContext").field("entries", &"<locked>").finish(),
}
}
}
#[derive(Debug, Clone)]
pub struct MatchInfo {
pub idiom: Idiom,
pub query: String,
}
#[derive(Debug, Clone)]
pub struct MatchesContext {
matches: HashMap<MatchRef, MatchInfo>,
table: Option<TableName>,
}
impl MatchesContext {
pub fn new() -> Self {
Self {
matches: HashMap::new(),
table: None,
}
}
pub fn set_table(&mut self, table: TableName) {
self.table = Some(table);
}
pub fn table(&self) -> Option<&TableName> {
self.table.as_ref()
}
pub fn insert(&mut self, match_ref: MatchRef, info: MatchInfo) {
self.matches.insert(match_ref, info);
}
pub fn get(&self, match_ref: MatchRef) -> Option<&MatchInfo> {
self.matches.get(&match_ref)
}
pub fn first(&self) -> Option<(MatchRef, &MatchInfo)> {
self.matches.iter().next().map(|(&k, v)| (k, v))
}
pub fn is_empty(&self) -> bool {
self.matches.is_empty()
}
pub fn resolve(&self, match_ref: MatchRef, table: TableName) -> Result<Arc<MatchContext>> {
let info = self.get(match_ref).or_else(|| {
if self.matches.len() == 1 {
self.first().map(|(_, info)| info)
} else {
None
}
});
match info {
Some(info) => {
Ok(Arc::new(MatchContext::new(info.idiom.clone(), info.query.clone(), table)))
}
None => {
if self.matches.is_empty() {
Err(anyhow::anyhow!("no MATCHES clause found in WHERE condition"))
} else {
Err(anyhow::anyhow!(
"no MATCHES clause found for match_ref {} (available: {:?})",
match_ref,
self.matches.keys().collect::<Vec<_>>()
))
}
}
}
}
}
impl Default for MatchesContext {
fn default() -> Self {
Self::new()
}
}