use datafusion::dataframe::DataFrame;
use datafusion::error::{DataFusionError, Result};
use datafusion::prelude::SessionContext;
use datafusion::sql::parser::Statement as DFStatement;
use datafusion::sql::sqlparser::ast::Statement as SqlStatement;
pub mod database;
mod from_first;
mod graph_ddl;
pub mod graph_table;
mod parser;
mod paths;
pub mod property_graph;
mod settings;
pub use database::open_session;
pub use parser::{
parse_sql, Attach, CreatePropertyGraph, DescribeGraph, DropPropertyGraph, UrsaParser,
UrsaStatement, DEFAULT_RECURSION_LIMIT,
};
pub use ursa_engine::database::OpenOptions;
pub fn parse_sql_for(ctx: &SessionContext, sql: &str) -> Result<Vec<UrsaStatement>> {
let options = ctx.state().config().options().sql_parser.clone();
let dialect = datafusion::sql::sqlparser::dialect::dialect_from_str(options.dialect)
.ok_or_else(|| {
DataFusionError::Configuration(format!("unknown SQL dialect '{}'", options.dialect))
})?;
UrsaParser::new(sql, dialect.as_ref(), options.recursion_limit.get())?.parse_statements()
}
pub fn parse_sql_with_text_for(
ctx: &SessionContext,
sql: &str,
) -> Result<Vec<(UrsaStatement, String)>> {
let options = ctx.state().config().options().sql_parser.clone();
let dialect = datafusion::sql::sqlparser::dialect::dialect_from_str(options.dialect)
.ok_or_else(|| {
DataFusionError::Configuration(format!("unknown SQL dialect '{}'", options.dialect))
})?;
UrsaParser::new(sql, dialect.as_ref(), options.recursion_limit.get())?
.parse_statements_with_text()
}
pub async fn execute(ctx: &SessionContext, statement: UrsaStatement) -> Result<DataFrame> {
execute_inner(ctx, statement, None).await
}
pub async fn execute_with_text(
ctx: &SessionContext,
statement: UrsaStatement,
text: &str,
) -> Result<DataFrame> {
execute_inner(ctx, statement, Some(text)).await
}
async fn execute_inner(
ctx: &SessionContext,
statement: UrsaStatement,
text: Option<&str>,
) -> Result<DataFrame> {
match ursa_engine::database::database(&ctx.state()) {
Some(db) => database::execute(ctx, &db, statement, text).await,
None => match statement {
UrsaStatement::Attach(attach) => database::attach(ctx, attach).await,
statement => run(ctx, statement).await,
},
}
}
pub(crate) async fn run(ctx: &SessionContext, statement: UrsaStatement) -> Result<DataFrame> {
match statement {
UrsaStatement::DataFusion(statement) => {
let state = ctx.state();
match statement.as_ref() {
DFStatement::CopyTo(copy) => {
ursa_engine::ensure_object_store_for(state.runtime_env(), ©.target).await?
}
DFStatement::CreateExternalTable(table) => {
for location in &table.locations {
ursa_engine::ensure_object_store_for(state.runtime_env(), location).await?
}
}
_ => {}
}
if let DFStatement::Statement(inner) = statement.as_ref() {
if let SqlStatement::ShowVariable { variable } = inner.as_ref() {
if let Some(frame) = settings::show(ctx, variable).await {
return frame;
}
}
if is_plain_show_tables(inner) {
return ctx
.sql(
"SELECT * FROM information_schema.tables \
WHERE table_schema <> 'information_schema'",
)
.await;
}
}
let reads = ursa_engine::read_functions::open_reads(&state, &statement).await?;
let plan = state.statement_to_plan(*statement).await?;
drop(reads);
ursa_engine::graph::check_ddl(&state, &plan).await?;
ctx.execute_logical_plan(plan).await
}
UrsaStatement::CreatePropertyGraph(statement) => graph_ddl::create(ctx, *statement).await,
UrsaStatement::DropPropertyGraph(statement) => graph_ddl::drop(ctx, statement),
UrsaStatement::ShowGraphs => graph_ddl::show(ctx),
UrsaStatement::DescribeGraph(statement) => graph_ddl::describe(ctx, statement).await,
UrsaStatement::Attach(_) => Err(DataFusionError::Plan(
"ATTACH cannot run while replaying a database's catalog".into(),
)),
}
}
fn is_plain_show_tables(statement: &SqlStatement) -> bool {
let SqlStatement::ShowTables {
terse: false,
history: false,
extended: false,
full: false,
external: false,
show_options,
} = statement
else {
return false;
};
show_options.show_in.is_none()
&& show_options.starts_with.is_none()
&& show_options.limit.is_none()
&& show_options.limit_from.is_none()
&& show_options.filter_position.is_none()
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn runs_datafusion_statements() {
let ctx = ursa_engine::new_session();
let mut statements = parse_sql_for(
&ctx,
"CREATE VIEW v AS SELECT 1 AS x UNION ALL SELECT 2; SELECT sum(x) AS s FROM v",
)
.unwrap();
let select = statements.pop().unwrap();
execute(&ctx, statements.pop().unwrap()).await.unwrap();
let batches = execute(&ctx, select)
.await
.unwrap()
.collect()
.await
.unwrap();
let formatted = datafusion::arrow::util::pretty::pretty_format_batches(&batches)
.unwrap()
.to_string();
assert!(formatted.contains("| 3 |"), "{formatted}");
}
#[tokio::test]
async fn show_tables_leaves_out_information_schema() {
let ctx = ursa_engine::new_session();
for sql in ["CREATE VIEW v AS SELECT 1 AS x", "SHOW TABLES"] {
let statement = parse_sql_for(&ctx, sql).unwrap().remove(0);
let batches = execute(&ctx, statement)
.await
.unwrap()
.collect()
.await
.unwrap();
if sql == "SHOW TABLES" {
let formatted = datafusion::arrow::util::pretty::pretty_format_batches(&batches)
.unwrap()
.to_string();
assert!(
formatted.contains("| public | v | VIEW"),
"{formatted}"
);
assert!(!formatted.contains("information_schema"), "{formatted}");
}
}
}
}