use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use crate::algebra::{Iri, Term as AlgebraTerm, Variable};
use crate::property_functions::{
PropFuncArg, PropertyFunction, PropertyFunctionContext, PropertyFunctionResult,
};
use super::index::TextSearchIndex;
pub const TEXT_QUERY_IRI: &str = "http://jena.apache.org/text#query";
pub struct TextQueryPropertyFunction {
index: Arc<TextSearchIndex>,
}
impl TextQueryPropertyFunction {
pub fn new(index: Arc<TextSearchIndex>) -> Self {
Self { index }
}
pub fn text_namespace() -> &'static str {
super::index::TEXT_NAMESPACE
}
pub fn iri() -> &'static str {
TEXT_QUERY_IRI
}
}
impl PropertyFunction for TextQueryPropertyFunction {
fn uri(&self) -> &str {
TEXT_QUERY_IRI
}
fn build(
&self,
_subject: &PropFuncArg,
predicate: &str,
object: &PropFuncArg,
_context: &PropertyFunctionContext,
) -> Result<()> {
if predicate != self.uri() {
bail!(
"Predicate mismatch: expected {}, got {}",
self.uri(),
predicate
);
}
match object {
PropFuncArg::List(items) if items.is_empty() => {
bail!("text:query requires at least one argument (query string)")
}
PropFuncArg::List(_) | PropFuncArg::Node(_) => Ok(()),
}
}
fn execute(
&self,
subject: &PropFuncArg,
_predicate: &str,
object: &PropFuncArg,
context: &PropertyFunctionContext,
) -> Result<PropertyFunctionResult> {
let subject = context.substitute(subject);
let object = context.substitute(object);
let subject_var: Option<Variable> = match &subject {
PropFuncArg::Node(AlgebraTerm::Variable(v)) => Some(v.clone()),
PropFuncArg::Node(AlgebraTerm::Iri(_)) => None, _ => bail!("text:query: subject must be a variable or an IRI"),
};
let bound_subject: Option<String> = match &subject {
PropFuncArg::Node(AlgebraTerm::Iri(iri)) => Some(iri.as_str().to_string()),
_ => None,
};
let args: Vec<AlgebraTerm> = match &object {
PropFuncArg::List(items) => items.clone(),
PropFuncArg::Node(AlgebraTerm::Literal(lit)) => {
vec![AlgebraTerm::Literal(lit.clone())]
}
_ => bail!("text:query: object must be a list of arguments"),
};
match args.as_slice() {
[AlgebraTerm::Literal(lit)] => {
self.execute_search(&subject_var, bound_subject.as_deref(), &lit.value, None, 10)
}
[AlgebraTerm::Literal(query_lit), AlgebraTerm::Literal(max_lit)] => {
let max = parse_max_results(&max_lit.value)?;
self.execute_search(
&subject_var,
bound_subject.as_deref(),
&query_lit.value,
None,
max,
)
}
[AlgebraTerm::Iri(pred_iri), AlgebraTerm::Literal(query_lit)] => self.execute_search(
&subject_var,
bound_subject.as_deref(),
&query_lit.value,
Some(pred_iri.as_str()),
10,
),
[AlgebraTerm::Iri(pred_iri), AlgebraTerm::Literal(query_lit), AlgebraTerm::Literal(max_lit)] =>
{
let max = parse_max_results(&max_lit.value)?;
self.execute_search(
&subject_var,
bound_subject.as_deref(),
&query_lit.value,
Some(pred_iri.as_str()),
max,
)
}
_ => bail!(
"text:query: unrecognised argument pattern — expected 1–3 args: \
[predIri?] queryString [maxResults?]"
),
}
}
}
impl TextQueryPropertyFunction {
fn execute_search(
&self,
subject_var: &Option<Variable>,
bound_subject: Option<&str>,
query_str: &str,
predicate_filter: Option<&str>,
max_results: usize,
) -> Result<PropertyFunctionResult> {
let hits = if let Some(pred) = predicate_filter {
self.index
.search_predicate(query_str, pred, max_results)
.map_err(|e| anyhow!("text:query search error: {e}"))?
} else {
self.index
.search(query_str, max_results)
.map_err(|e| anyhow!("text:query search error: {e}"))?
};
match (subject_var, bound_subject) {
(Some(var), None) => {
let solutions: Vec<HashMap<Variable, AlgebraTerm>> = hits
.into_iter()
.filter_map(|hit| {
Iri::new(&hit.subject_iri).ok().map(|iri| {
let mut bindings = HashMap::new();
bindings.insert(var.clone(), AlgebraTerm::Iri(iri));
bindings
})
})
.collect();
Ok(PropertyFunctionResult::Multiple(solutions))
}
(None, Some(bound_iri)) => {
let matched = hits.iter().any(|hit| hit.subject_iri == bound_iri);
Ok(PropertyFunctionResult::Boolean(matched))
}
_ => bail!("text:query: internal argument state error"),
}
}
}
fn parse_max_results(s: &str) -> Result<usize> {
s.trim().parse::<usize>().map_err(|_| {
anyhow!(
"text:query: maxResults must be a non-negative integer, got {:?}",
s
)
})
}