use std::sync::{Arc, OnceLock};
use datafusion::arrow::array::{Array, BooleanArray, StringArray};
use datafusion::arrow::datatypes::DataType;
use datafusion::common::Result;
use datafusion::logical_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility};
use datafusion::scalar::ScalarValue;
pub const OS_MATCH: &str = "os_match";
pub const OS_GEO_BBOX: &str = "os_geo_bbox";
pub const OS_GEO_DISTANCE: &str = "os_geo_distance";
pub const OS_GEO_POLYGON: &str = "os_geo_polygon";
pub fn tokens(s: &str) -> Vec<String> {
s.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(str::to_lowercase)
.collect()
}
fn matches(doc: &str, query: &str) -> bool {
let q = tokens(query);
!q.is_empty() && {
let d = tokens(doc);
q.iter().any(|t| d.contains(t))
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct MatchUdf {
signature: Signature,
}
impl MatchUdf {
fn new() -> Self {
Self {
signature: Signature::exact(vec![DataType::Utf8, DataType::Utf8], Volatility::Immutable),
}
}
}
impl ScalarUDFImpl for MatchUdf {
fn name(&self) -> &str {
OS_MATCH
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _: &[DataType]) -> Result<DataType> {
Ok(DataType::Boolean)
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let n = args.number_rows;
let query = match args.args.get(1) {
Some(ColumnarValue::Scalar(ScalarValue::Utf8(Some(q)))) => q.clone(),
_ => return Ok(ColumnarValue::Array(Arc::new(BooleanArray::from(vec![false; n])))),
};
let col = args.args[0].clone().into_array(n)?;
let out: BooleanArray = match col.as_any().downcast_ref::<StringArray>() {
Some(sa) => (0..sa.len())
.map(|i| Some(!sa.is_null(i) && matches(sa.value(i), &query)))
.collect(),
None => BooleanArray::from(vec![false; n]),
};
Ok(ColumnarValue::Array(Arc::new(out)))
}
}
pub fn os_match_udf() -> Arc<ScalarUDF> {
static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
UDF.get_or_init(|| Arc::new(ScalarUDF::from(MatchUdf::new()))).clone()
}
#[derive(Debug, PartialEq, Eq, Hash)]
struct GeoUdf {
name: &'static str,
signature: Signature,
}
impl ScalarUDFImpl for GeoUdf {
fn name(&self) -> &str {
self.name
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _: &[DataType]) -> Result<DataType> {
Ok(DataType::Boolean)
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
Ok(ColumnarValue::Array(Arc::new(BooleanArray::from(vec![
false;
args.number_rows
]))))
}
}
fn geo_udf(name: &'static str, arity: usize) -> Arc<ScalarUDF> {
Arc::new(ScalarUDF::from(GeoUdf {
name,
signature: Signature::any(arity, Volatility::Immutable),
}))
}
pub fn os_geo_bbox_udf() -> Arc<ScalarUDF> {
static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
UDF.get_or_init(|| geo_udf(OS_GEO_BBOX, 5)).clone()
}
pub fn os_geo_distance_udf() -> Arc<ScalarUDF> {
static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
UDF.get_or_init(|| geo_udf(OS_GEO_DISTANCE, 4)).clone()
}
pub fn os_geo_polygon_udf() -> Arc<ScalarUDF> {
static UDF: OnceLock<Arc<ScalarUDF>> = OnceLock::new();
UDF.get_or_init(|| geo_udf(OS_GEO_POLYGON, 2)).clone()
}
pub fn all_udfs() -> [Arc<ScalarUDF>; 4] {
[
os_match_udf(),
os_geo_bbox_udf(),
os_geo_distance_udf(),
os_geo_polygon_udf(),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokenise_splits_lowercases_and_drops_empties() {
assert_eq!(tokens("Engine Overheating!"), vec!["engine", "overheating"]);
assert_eq!(tokens("fault(sensor,7)"), vec!["fault", "sensor", "7"]);
assert!(tokens(" ").is_empty());
}
#[test]
fn or_token_match_is_case_insensitive_and_not_substring() {
assert!(matches("engine overheating detected", "ENGINE")); assert!(matches("brakes worn", "engine brakes")); assert!(!matches("engine", "eng")); assert!(!matches("anything", " ")); }
#[test]
fn udfs_are_named_and_boolean() {
let udf = os_match_udf();
assert_eq!(udf.name(), OS_MATCH);
assert_eq!(
udf.return_type(&[DataType::Utf8, DataType::Utf8]).unwrap(),
DataType::Boolean
);
let names: Vec<String> = all_udfs().iter().map(|u| u.name().to_string()).collect();
assert_eq!(names, [OS_MATCH, OS_GEO_BBOX, OS_GEO_DISTANCE, OS_GEO_POLYGON]);
}
}