#![cfg(feature = "sql")]
#![allow(dead_code)]
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use datafusion::{
arrow::{array::RecordBatch, datatypes::SchemaRef},
catalog::SchemaProvider,
execution::{
context::{SessionContext, SessionState},
options::CsvReadOptions,
SessionStateBuilder,
},
physical_plan::{stream::RecordBatchStreamAdapter, PhysicalExpr, SendableRecordBatchStream},
prelude::DataFrame,
sql::unparser::dialect::{DefaultDialect, Dialect},
};
use futures::TryStreamExt;
use datafusion_federation::sql::{SQLExecutor, SQLFederationProvider, SQLSchemaProvider};
pub struct RecordingSQLExecutor {
name: &'static str,
context: &'static str,
session: Arc<SessionContext>,
queries: Arc<Mutex<Vec<String>>>,
}
impl RecordingSQLExecutor {
pub fn new(name: &'static str, context: &'static str, session: Arc<SessionContext>) -> Self {
Self {
name,
context,
session,
queries: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn queries(&self) -> Arc<Mutex<Vec<String>>> {
self.queries.clone()
}
}
#[async_trait]
impl SQLExecutor for RecordingSQLExecutor {
fn name(&self) -> &str {
self.name
}
fn compute_context(&self) -> Option<String> {
Some(self.context.to_string())
}
fn execute(
&self,
sql: &str,
schema: SchemaRef,
_filters: &[Arc<dyn PhysicalExpr>],
) -> datafusion::error::Result<SendableRecordBatchStream> {
self.queries.lock().unwrap().push(sql.to_string());
let session = self.session.clone();
let sql = sql.to_string();
let future_stream = async move { session.sql(&sql).await?.execute_stream().await };
let stream = futures::stream::once(future_stream).try_flatten();
Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
}
async fn table_names(&self) -> datafusion::error::Result<Vec<String>> {
Err(datafusion::error::DataFusionError::NotImplemented(
"table inference not implemented".to_string(),
))
}
async fn get_table_schema(&self, table_name: &str) -> datafusion::error::Result<SchemaRef> {
let sql = format!("select * from {table_name} limit 1");
let df = self.session.sql(&sql).await?;
Ok(Arc::new(df.schema().as_arrow().clone()))
}
fn dialect(&self) -> Arc<dyn Dialect> {
Arc::new(DefaultDialect {})
}
}
pub fn data_path(file: &str) -> String {
format!("{}/tests/data/{file}", env!("CARGO_MANIFEST_DIR"))
}
pub async fn remote_ctx(table_name: &str, csv: &str) -> Arc<SessionContext> {
let ctx = Arc::new(SessionContext::new());
ctx.register_csv(table_name, data_path(csv), CsvReadOptions::new())
.await
.expect("register csv");
ctx
}
pub fn overwrite_default_schema(state: &SessionState, schema: Arc<dyn SchemaProvider>) {
let options = &state.config().options().catalog;
let catalog = state
.catalog_list()
.catalog(options.default_catalog.as_str())
.expect("default catalog");
catalog
.register_schema(options.default_schema.as_str(), schema)
.expect("register schema");
}
pub async fn schema_provider(
executor: Arc<dyn SQLExecutor>,
tables: &[&str],
) -> Arc<SQLSchemaProvider> {
let provider = Arc::new(SQLFederationProvider::new(executor));
let tables: Vec<String> = tables.iter().map(|t| t.to_string()).collect();
Arc::new(
SQLSchemaProvider::new_with_tables(provider, tables)
.await
.expect("schema provider"),
)
}
pub async fn run_federated(schema: Arc<dyn SchemaProvider>, query: &str) -> Vec<RecordBatch> {
let state = datafusion_federation::default_session_state();
overwrite_default_schema(&state, schema);
let ctx = SessionContext::new_with_state(state);
collect(ctx.sql(query).await.expect("plan query")).await
}
pub async fn collect(df: DataFrame) -> Vec<RecordBatch> {
df.collect().await.expect("collect")
}
pub async fn try_run_without_federation(
schema: Arc<dyn SchemaProvider>,
query: &str,
) -> datafusion::error::Result<Vec<RecordBatch>> {
let state = SessionStateBuilder::new().with_default_features().build();
overwrite_default_schema(&state, schema);
let ctx = SessionContext::new_with_state(state);
ctx.sql(query).await?.collect().await
}
pub fn row_count(batches: &[RecordBatch]) -> usize {
batches.iter().map(RecordBatch::num_rows).sum()
}
pub fn recorded_sql(queries: &Arc<Mutex<Vec<String>>>) -> String {
queries.lock().unwrap().join("\n").to_lowercase()
}