use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
use akar_common::types::{PhysicalTypeID, Value};
use akar_common::vector::{DataChunk, ValueVector};
use akar_storage::table::TableCatalog;
use std::sync::Arc;
pub struct PhysicalCountRelTable {
pub table_name: String,
pub table_id: u64,
pub table_catalog: Option<Arc<TableCatalog>>,
}
impl PhysicalOperatorExec for PhysicalCountRelTable {
fn operator_type(&self) -> &str {
"count_rel_table"
}
fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
let tc = self
.table_catalog
.as_ref()
.ok_or_else(|| "No table catalog for CountRelTable".to_string())?;
let count = if let Some(table) = tc.get_rel_table(self.table_id) {
table.num_rows as i64
} else {
0
};
let mut v = ValueVector::new(PhysicalTypeID::Int64, 1);
v.resize(1);
v.set_i64(0, count);
let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&v).array;
Ok(vec![DataChunk::new(vec![arr], vec![PhysicalTypeID::Int64])])
}
}
pub struct PhysicalCreateFtsIndex {
pub index_name: String,
pub table_name: String,
pub column_name: String,
pub tokenizer: Option<String>,
pub if_not_exists: bool,
pub table_catalog: Arc<TableCatalog>,
}
impl PhysicalOperatorExec for PhysicalCreateFtsIndex {
fn operator_type(&self) -> &str {
"create_fts_index"
}
fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
let (columns, col_idx, num_rows, source_data) = {
let source_table = match self.table_catalog.get_node_table_by_name(&self.table_name) {
Some(t) => t,
None => return Err(format!("Table '{}' not found", self.table_name).into()),
};
let col_idx = source_table
.columns
.iter()
.position(|c| c.name == self.column_name)
.ok_or_else(|| format!("Column '{}' not found in '{}'", self.column_name, self.table_name))?;
(
source_table.columns.clone(),
col_idx,
source_table.num_rows as usize,
source_table.to_column_major_data(),
)
};
let mut rows: Vec<(i64, String)> = Vec::with_capacity(num_rows);
for row_idx in 0..num_rows {
let text = source_data
.get(col_idx)
.and_then(|col| col.get(row_idx))
.and_then(|v| match v {
Value::String(s) => Some(s.clone()),
_ => None,
})
.unwrap_or_default();
rows.push((row_idx as i64, text));
}
let tokenizer = akar_fts::tokenizer::resolve(self.tokenizer.as_deref())?;
let index_dir = self
.table_catalog
.db_path()
.filter(|p| p.to_string_lossy() != ":memory:")
.map(|p| p.join("fts").join(&self.index_name));
akar_fts::build::build_index(&columns, &self.column_name, &tokenizer, index_dir.as_deref(), &rows)?;
let mut result_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::String, 1);
result_vec.resize(1);
result_vec
.set_value(
0,
&Value::String(format!("FTS index '{}' built successfully.", self.index_name)),
)
.unwrap();
let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&result_vec).array;
let mut result = DataChunk::new(vec![arr], vec![akar_common::types::PhysicalTypeID::String]);
result.size = 1;
result.field_names = vec!["result".to_string()];
Ok(vec![result])
}
}
#[derive(Debug, Clone)]
pub struct PhysicalFtsScan {
pub index_name: String,
pub query_string: String,
pub table_name: String,
pub column_name: String,
pub table_catalog: Arc<TableCatalog>,
}
impl PhysicalOperatorExec for PhysicalFtsScan {
fn operator_type(&self) -> &str {
"fts_scan"
}
fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
let index_dir = self.index_dir().ok_or_else(|| {
"FTS scan requires a disk-backed database (the FTS index lives on disk; in-memory DBs are not supported — P104.2)"
.to_string()
})?;
let reader = self.open_reader(&index_dir)?;
let searcher = reader.searcher();
let search_schema = searcher.schema();
let text_field = search_schema.get_field(&self.column_name).map_err(|_| {
format!(
"FTS: column '{}' not found in index '{}'",
self.column_name, self.index_name
)
})?;
let doc_id_field = search_schema
.get_field(akar_fts::schema::DOC_ID_FIELD)
.map_err(|_| "FTS: internal doc_id field missing".to_string())?;
let limit = searcher.num_docs() as usize;
let hits = akar_fts::index::TantivyIndex::search_doc_ids(
&reader,
&self.query_string,
vec![text_field],
doc_id_field,
limit,
)
.map_err(|e| format!("FTS: search '{}': {e}", self.query_string))?;
let source_table = self.table_catalog.get_node_table_by_name(&self.table_name);
let source_col = source_table
.as_ref()
.and_then(|t| t.columns.iter().position(|c| c.name == self.column_name));
let doc_valid = |doc_id: i64| -> bool {
let Ok(r) = usize::try_from(doc_id) else {
return false;
};
match (&source_table, source_col) {
(Some(t), Some(ci)) => r < t.num_rows as usize && matches!(t.get_value(r, ci), Some(Value::String(_))),
_ => true, }
};
let mut ranked: Vec<(i64, f64)> = Vec::with_capacity(hits.len());
for (doc_id, score) in hits {
if doc_valid(doc_id) {
ranked.push((doc_id, score as f64));
}
}
let n = ranked.len();
let mut id_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::Int64, n);
let mut score_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::Double, n);
id_vec.resize(n);
score_vec.resize(n);
for (i, (doc_id, score)) in ranked.into_iter().enumerate() {
id_vec.set_i64(i, doc_id);
score_vec.set_double(i, score);
}
let arr1 = akar_common::arrow_vector::ArrowVector::from_legacy(&id_vec).array;
let arr2 = akar_common::arrow_vector::ArrowVector::from_legacy(&score_vec).array;
let mut chunk = DataChunk::new(
vec![arr1, arr2],
vec![
akar_common::types::PhysicalTypeID::Int64,
akar_common::types::PhysicalTypeID::Double,
],
);
chunk.size = n;
chunk.field_names = vec!["doc_id".to_string(), "score".to_string()];
Ok(vec![chunk])
}
}
impl PhysicalFtsScan {
fn index_dir(&self) -> Option<std::path::PathBuf> {
self.table_catalog
.db_path()
.filter(|p| p.to_string_lossy() != ":memory:")
.map(|p| p.join("fts").join(&self.index_name))
}
fn open_reader(&self, index_dir: &std::path::Path) -> Result<akar_fts::index::IndexReader, String> {
if !index_dir.join("meta.json").exists() {
return Err(format!(
"FTS index '{}' not found on disk at '{}' (run CREATE FTS INDEX first)",
self.index_name,
index_dir.display()
));
}
let handle = akar_fts::index::runtime_handle(&self.table_catalog, &self.index_name, index_dir)?;
handle.reader().map_err(|e| format!("FTS: reader: {e}"))
}
}