use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use datafusion::arrow::datatypes::SchemaRef;
use datafusion::catalog::{Session, TableProvider};
use datafusion::datasource::memory::MemorySourceConfig;
use datafusion::error::Result as DataFusionResult;
use datafusion::logical_expr::{Expr, TableProviderFilterPushDown, TableType};
use datafusion::physical_plan::ExecutionPlan;
use serde_json::{json, Value};
use crate::client::OpenSearchClient;
use crate::schema::{build_batch, schema_from_mapping};
use crate::{pushdown, MalformedMappingSnafu, Result};
pub const DEFAULT_SIZE: usize = 10_000;
pub struct OpenSearchTableProvider {
client: OpenSearchClient,
index: String,
schema: SchemaRef,
default_size: usize,
base_filter: Vec<Value>,
sort: Vec<(String, bool)>,
search_after: Option<Vec<Value>>,
}
impl fmt::Debug for OpenSearchTableProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpenSearchTableProvider")
.field("index", &self.index)
.field("fields", &self.schema.fields().len())
.field("base_filter", &self.base_filter.len())
.finish()
}
}
impl OpenSearchTableProvider {
pub fn new(base_url: impl Into<String>, index: impl Into<String>, schema: SchemaRef) -> Self {
Self::with_client_and_schema(OpenSearchClient::new(base_url), index, schema)
}
pub fn with_client_and_schema(client: OpenSearchClient, index: impl Into<String>, schema: SchemaRef) -> Self {
Self {
client,
index: index.into(),
schema,
default_size: DEFAULT_SIZE,
base_filter: Vec::new(),
sort: Vec::new(),
search_after: None,
}
}
pub fn with_sort(mut self, sort: Vec<(String, bool)>) -> Self {
self.sort = sort;
self
}
pub fn with_search_after(mut self, after: Vec<Value>) -> Self {
self.search_after = Some(after);
self
}
pub fn with_default_size(mut self, n: usize) -> Self {
self.default_size = n;
self
}
pub fn with_base_filter(mut self, clauses: Vec<Value>) -> Self {
self.base_filter = clauses;
self
}
pub fn with_client(mut self, http: reqwest::Client) -> Self {
self.client = self.client.with_client(http);
self
}
pub fn index(&self) -> &str {
&self.index
}
}
#[async_trait]
impl TableProvider for OpenSearchTableProvider {
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
fn table_type(&self) -> TableType {
TableType::Base
}
fn supports_filters_pushdown(&self, filters: &[&Expr]) -> DataFusionResult<Vec<TableProviderFilterPushDown>> {
Ok(filters
.iter()
.map(|f| {
if pushdown::expr_to_query(f).is_some() {
TableProviderFilterPushDown::Exact
} else {
TableProviderFilterPushDown::Unsupported
}
})
.collect())
}
async fn scan(
&self,
_state: &dyn Session,
projection: Option<&Vec<usize>>,
filters: &[Expr],
limit: Option<usize>,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
let projected: SchemaRef = match projection {
Some(p) => Arc::new(self.schema.project(p)?),
None => self.schema.clone(),
};
let mut clauses: Vec<Value> = self.base_filter.clone();
clauses.extend(filters.iter().filter_map(pushdown::expr_to_query));
let query = if clauses.is_empty() {
json!({ "match_all": {} })
} else {
json!({ "bool": { "filter": clauses } })
};
let source_fields: Vec<&str> = projected.fields().iter().map(|f| f.name().as_str()).collect();
let mut body = json!({
"size": limit.unwrap_or(self.default_size),
"query": query,
"_source": source_fields,
});
if !self.sort.is_empty() {
body["sort"] = Value::Array(
self.sort
.iter()
.map(|(f, asc)| json!({ f: { "order": if *asc { "asc" } else { "desc" } } }))
.collect(),
);
}
if let Some(after) = &self.search_after {
body["search_after"] = Value::Array(after.clone());
}
let sources = self.client.search(&self.index, &body).await?;
let batch = build_batch(&projected, &sources)?;
let exec = MemorySourceConfig::try_new_exec(&[vec![batch]], projected, None)?;
Ok(exec as Arc<dyn ExecutionPlan>)
}
}
#[derive(Clone, Debug)]
pub struct OpenSearchTableFactory {
client: OpenSearchClient,
}
impl OpenSearchTableFactory {
pub fn new(client: OpenSearchClient) -> Self {
Self { client }
}
pub async fn table_provider(&self, index: impl Into<String>) -> Result<Arc<dyn TableProvider>> {
let index = index.into();
let mapping = self.client.mapping(&index).await?;
let schema =
schema_from_mapping(&mapping).ok_or_else(|| MalformedMappingSnafu { index: index.clone() }.build())?;
Ok(Arc::new(OpenSearchTableProvider::with_client_and_schema(
self.client.clone(),
index,
schema,
)))
}
pub fn table_provider_with_schema(&self, index: impl Into<String>, schema: SchemaRef) -> Arc<dyn TableProvider> {
Arc::new(OpenSearchTableProvider::with_client_and_schema(
self.client.clone(),
index,
schema,
))
}
pub fn provider_with_schema(&self, index: impl Into<String>, schema: SchemaRef) -> OpenSearchTableProvider {
OpenSearchTableProvider::with_client_and_schema(self.client.clone(), index, schema)
}
}
#[cfg(test)]
mod tests {
use super::*;
use datafusion::arrow::datatypes::{DataType, Field, Schema};
fn schema() -> SchemaRef {
Arc::new(Schema::new(vec![
Field::new("id", DataType::Utf8, true),
Field::new("status", DataType::Utf8, true),
Field::new("speed", DataType::Float64, true),
Field::new("active", DataType::Boolean, true),
]))
}
#[test]
fn projected_schema_selects_a_subset() {
let proj = schema().project(&[0, 1]).unwrap();
assert_eq!(proj.fields().len(), 2);
assert_eq!(proj.field(0).name(), "id");
assert_eq!(proj.field(1).name(), "status");
}
#[test]
fn with_base_filter_is_recorded() {
let p = OpenSearchTableProvider::new("http://x", "i", schema())
.with_base_filter(vec![json!({ "term": { "tenant": "t1" } })]);
assert_eq!(p.base_filter.len(), 1);
assert_eq!(p.index(), "i");
}
#[test]
fn factory_builds_a_declared_schema_provider_without_io() {
let f = OpenSearchTableFactory::new(OpenSearchClient::new("http://x:9200"));
let p = f.provider_with_schema("idx", schema()).with_default_size(5);
assert_eq!(p.default_size, 5);
assert_eq!(f.table_provider_with_schema("idx", schema()).schema().fields().len(), 4);
}
}