use datafusion::sql::sqlparser::ast::{
ObjectName, PipeOperator, Query, Select, Statement, Visit, Visitor,
};
use datafusion::sql::sqlparser::dialect::GenericDialect;
use datafusion::sql::sqlparser::parser::Parser;
use std::ops::ControlFlow;
#[derive(Debug, thiserror::Error)]
pub(crate) enum StatementRejected {
#[error("statement kind not allowed: {0}")]
DisallowedKind(String),
#[error("could not parse SQL: {0}")]
ParseError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AllowedStatement {
Query,
Explain,
}
pub(crate) fn check_statement_allowed(
sql: &str,
allow_explain: bool,
) -> Result<AllowedStatement, StatementRejected> {
let statements = Parser::parse_sql(&GenericDialect {}, sql)
.map_err(|err| StatementRejected::ParseError(err.to_string()))?;
match statements.as_slice() {
[Statement::Query(query)] => {
reject_ordered_information_schema(query)?;
Ok(AllowedStatement::Query)
}
[Statement::Explain { analyze: true, .. }] => Err(StatementRejected::DisallowedKind(
"EXPLAIN ANALYZE executes the query and is never allowed".to_string(),
)),
[
Statement::Explain {
analyze: false,
statement,
..
},
] => {
if !allow_explain {
return Err(StatementRejected::DisallowedKind(
"EXPLAIN is not enabled on this surface".to_string(),
));
}
match statement.as_ref() {
Statement::Query(_) => Ok(AllowedStatement::Explain),
other => Err(StatementRejected::DisallowedKind(format!(
"EXPLAIN of a {} statement is not allowed",
statement_kind(other)
))),
}
}
[other] => Err(StatementRejected::DisallowedKind(format!(
"{} statements are not allowed",
statement_kind(other)
))),
[] => Err(StatementRejected::DisallowedKind(
"no statement found".to_string(),
)),
multiple => Err(StatementRejected::DisallowedKind(format!(
"expected exactly one statement, found {}",
multiple.len()
))),
}
}
fn reject_ordered_information_schema(query: &Query) -> Result<(), StatementRejected> {
let mut scan = InformationSchemaOrderingScan::default();
let _: ControlFlow<()> = query.visit(&mut scan);
if scan.has_information_schema_reference && scan.has_ordering {
return Err(StatementRejected::DisallowedKind(
"ordering an information_schema query is not supported: it can silently drop rows \
from the result — remove the ORDER BY (or SORT BY, or pipe ORDER BY) and query \
information_schema without ordering it"
.to_string(),
));
}
Ok(())
}
#[derive(Debug, Default)]
struct InformationSchemaOrderingScan {
has_information_schema_reference: bool,
has_ordering: bool,
}
impl Visitor for InformationSchemaOrderingScan {
type Break = ();
fn pre_visit_relation(&mut self, relation: &ObjectName) -> ControlFlow<Self::Break> {
if references_information_schema(relation) {
self.has_information_schema_reference = true;
}
ControlFlow::Continue(())
}
fn pre_visit_query(&mut self, query: &Query) -> ControlFlow<Self::Break> {
if query.order_by.is_some()
|| query
.pipe_operators
.iter()
.any(|operator| matches!(operator, PipeOperator::OrderBy { .. }))
{
self.has_ordering = true;
}
ControlFlow::Continue(())
}
fn pre_visit_select(&mut self, select: &Select) -> ControlFlow<Self::Break> {
if !select.sort_by.is_empty() {
self.has_ordering = true;
}
ControlFlow::Continue(())
}
}
fn references_information_schema(relation: &ObjectName) -> bool {
relation.0.iter().any(|part| {
part.as_ident()
.is_some_and(|ident| ident.value.eq_ignore_ascii_case("information_schema"))
})
}
fn statement_kind(statement: &Statement) -> String {
statement
.to_string()
.split_whitespace()
.next()
.unwrap_or("unknown")
.trim_end_matches(|c: char| !c.is_ascii_alphanumeric())
.to_ascii_uppercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn select_is_allowed() {
assert_eq!(
check_statement_allowed("SELECT 1", false).expect("must be allowed"),
AllowedStatement::Query
);
}
#[test]
fn with_select_is_allowed() {
assert_eq!(
check_statement_allowed("WITH t AS (SELECT 1) SELECT * FROM t", false)
.expect("must be allowed"),
AllowedStatement::Query
);
}
#[test]
fn explain_select_allowed_when_opted_in() {
assert_eq!(
check_statement_allowed("EXPLAIN SELECT 1", true).expect("must be allowed"),
AllowedStatement::Explain
);
}
#[test]
fn explain_select_rejected_without_opt_in() {
let err = check_statement_allowed("EXPLAIN SELECT 1", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn explain_analyze_rejected_with_opt_in() {
let err = check_statement_allowed("EXPLAIN ANALYZE SELECT 1", true).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn explain_analyze_rejected_without_opt_in() {
let err = check_statement_allowed("EXPLAIN ANALYZE SELECT 1", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn explain_of_insert_rejected_even_with_opt_in() {
let err = check_statement_allowed("EXPLAIN INSERT INTO t VALUES (1)", true).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn create_table_rejected() {
let err = check_statement_allowed("CREATE TABLE t (a INT)", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn drop_table_rejected() {
let err = check_statement_allowed("DROP TABLE t", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn insert_rejected() {
let err = check_statement_allowed("INSERT INTO t VALUES (1)", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn update_rejected() {
let err = check_statement_allowed("UPDATE t SET a = 1", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn delete_rejected() {
let err = check_statement_allowed("DELETE FROM t", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn show_rejected() {
let err = check_statement_allowed("SHOW TABLES", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn copy_rejected() {
let err = check_statement_allowed("COPY t TO 'out.csv'", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn set_rejected() {
let err = check_statement_allowed("SET timezone = 'UTC'", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn multi_statement_batch_rejected() {
let err = check_statement_allowed("SELECT 1; SELECT 2", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn empty_string_rejected() {
let err = check_statement_allowed("", false).unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn garbage_rejected_as_parse_error() {
let err = check_statement_allowed("not even close to sql (((", false).unwrap_err();
assert!(matches!(err, StatementRejected::ParseError(_)));
}
#[test]
fn information_schema_direct_order_by_rejected() {
let err = check_statement_allowed(
"SELECT table_name FROM information_schema.tables ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn catalog_qualified_information_schema_order_by_rejected() {
let err = check_statement_allowed(
"SELECT table_name FROM datafusion.information_schema.tables ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn quoted_mixed_case_information_schema_order_by_rejected() {
let err = check_statement_allowed(
r#"SELECT "TABLES"."TABLE_NAME" FROM "Information_Schema"."TABLES" ORDER BY "TABLES"."TABLE_NAME""#,
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn aliased_information_schema_order_by_rejected() {
let err = check_statement_allowed(
"SELECT t.table_name FROM information_schema.tables t ORDER BY t.table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn join_with_information_schema_order_by_rejected() {
let err = check_statement_allowed(
"SELECT t.table_name FROM information_schema.tables t \
JOIN information_schema.columns c ON t.table_name = c.table_name \
ORDER BY t.table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn ordering_in_subquery_over_information_schema_rejected() {
let err = check_statement_allowed(
"SELECT * FROM (SELECT table_name FROM information_schema.tables \
ORDER BY table_name) sub",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn outer_order_by_over_cte_reading_information_schema_rejected() {
let err = check_statement_allowed(
"WITH t AS (SELECT table_name FROM information_schema.tables) \
SELECT * FROM t ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn pipe_style_order_by_over_information_schema_rejected() {
let err = check_statement_allowed(
"SELECT table_name FROM information_schema.tables |> ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn union_with_information_schema_arm_and_outer_order_by_rejected() {
let err = check_statement_allowed(
"SELECT table_name FROM information_schema.tables \
UNION SELECT name FROM usage ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn outer_order_by_over_derived_subquery_reading_information_schema_rejected() {
let err = check_statement_allowed(
"SELECT table_name FROM (SELECT table_name FROM information_schema.tables) sub \
ORDER BY table_name",
false,
)
.unwrap_err();
assert!(matches!(err, StatementRejected::DisallowedKind(_)));
}
#[test]
fn information_schema_without_order_by_allowed() {
assert_eq!(
check_statement_allowed("SELECT table_name FROM information_schema.tables", false)
.expect("must be allowed"),
AllowedStatement::Query
);
}
#[test]
fn order_by_over_ordinary_table_allowed() {
assert_eq!(
check_statement_allowed("SELECT * FROM usage ORDER BY position", false)
.expect("must be allowed"),
AllowedStatement::Query
);
}
#[test]
fn explain_over_ordered_information_schema_allowed() {
assert_eq!(
check_statement_allowed(
"EXPLAIN SELECT table_name FROM information_schema.tables ORDER BY table_name",
true,
)
.expect("must be allowed"),
AllowedStatement::Explain
);
}
}