-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Upgrade sqlparser-rs to 0.51.0, support new interval logic from sqlparse-rs
#12222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
179be76
edf318b
ab88318
6bba073
268d478
8fe6a2f
5ad18e0
e7ed21e
ce3425a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ use datafusion_expr::expr::{BinaryExpr, Placeholder}; | |
use datafusion_expr::planner::PlannerResult; | ||
use datafusion_expr::{lit, Expr, Operator}; | ||
use log::debug; | ||
use sqlparser::ast::{BinaryOperator, Expr as SQLExpr, Interval, Value}; | ||
use sqlparser::ast::{BinaryOperator, Expr as SQLExpr, Interval, UnaryOperator, Value}; | ||
use sqlparser::parser::ParserError::ParserError; | ||
use std::borrow::Cow; | ||
|
||
|
@@ -168,12 +168,11 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { | |
|
||
/// Convert a SQL interval expression to a DataFusion logical plan | ||
/// expression | ||
#[allow(clippy::only_used_in_recursion)] | ||
pub(super) fn sql_interval_to_expr( | ||
&self, | ||
negative: bool, | ||
interval: Interval, | ||
schema: &DFSchema, | ||
planner_context: &mut PlannerContext, | ||
) -> Result<Expr> { | ||
if interval.leading_precision.is_some() { | ||
return not_impl_err!( | ||
|
@@ -196,127 +195,42 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { | |
); | ||
} | ||
|
||
// Only handle string exprs for now | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is great to remove all this stuff from the sql planner (as it is now in the parser) |
||
let value = match *interval.value { | ||
SQLExpr::Value( | ||
Value::SingleQuotedString(s) | Value::DoubleQuotedString(s), | ||
) => { | ||
if negative { | ||
format!("-{s}") | ||
} else { | ||
s | ||
} | ||
} | ||
// Support expressions like `interval '1 month' + date/timestamp`. | ||
// Such expressions are parsed like this by sqlparser-rs | ||
// | ||
// Interval | ||
// BinaryOp | ||
// Value(StringLiteral) | ||
// Cast | ||
// Value(StringLiteral) | ||
// | ||
// This code rewrites them to the following: | ||
// | ||
// BinaryOp | ||
// Interval | ||
// Value(StringLiteral) | ||
// Cast | ||
// Value(StringLiteral) | ||
SQLExpr::BinaryOp { left, op, right } => { | ||
let df_op = match op { | ||
BinaryOperator::Plus => Operator::Plus, | ||
BinaryOperator::Minus => Operator::Minus, | ||
BinaryOperator::Eq => Operator::Eq, | ||
BinaryOperator::NotEq => Operator::NotEq, | ||
BinaryOperator::Gt => Operator::Gt, | ||
BinaryOperator::GtEq => Operator::GtEq, | ||
BinaryOperator::Lt => Operator::Lt, | ||
BinaryOperator::LtEq => Operator::LtEq, | ||
_ => { | ||
return not_impl_err!("Unsupported interval operator: {op:?}"); | ||
} | ||
}; | ||
match ( | ||
interval.leading_field.as_ref(), | ||
left.as_ref(), | ||
right.as_ref(), | ||
) { | ||
(_, _, SQLExpr::Value(_)) => { | ||
let left_expr = self.sql_interval_to_expr( | ||
negative, | ||
Interval { | ||
value: left, | ||
leading_field: interval.leading_field.clone(), | ||
leading_precision: None, | ||
last_field: None, | ||
fractional_seconds_precision: None, | ||
}, | ||
schema, | ||
planner_context, | ||
)?; | ||
let right_expr = self.sql_interval_to_expr( | ||
false, | ||
Interval { | ||
value: right, | ||
leading_field: interval.leading_field, | ||
leading_precision: None, | ||
last_field: None, | ||
fractional_seconds_precision: None, | ||
}, | ||
schema, | ||
planner_context, | ||
)?; | ||
return Ok(Expr::BinaryExpr(BinaryExpr::new( | ||
Box::new(left_expr), | ||
df_op, | ||
Box::new(right_expr), | ||
))); | ||
} | ||
// In this case, the left node is part of the interval | ||
// expr and the right node is an independent expr. | ||
// | ||
// Leading field is not supported when the right operand | ||
// is not a value. | ||
(None, _, _) => { | ||
let left_expr = self.sql_interval_to_expr( | ||
negative, | ||
Interval { | ||
value: left, | ||
leading_field: None, | ||
leading_precision: None, | ||
last_field: None, | ||
fractional_seconds_precision: None, | ||
}, | ||
schema, | ||
planner_context, | ||
)?; | ||
let right_expr = self.sql_expr_to_logical_expr( | ||
*right, | ||
schema, | ||
planner_context, | ||
)?; | ||
return Ok(Expr::BinaryExpr(BinaryExpr::new( | ||
Box::new(left_expr), | ||
df_op, | ||
Box::new(right_expr), | ||
))); | ||
} | ||
_ => { | ||
let value = SQLExpr::BinaryOp { left, op, right }; | ||
return not_impl_err!( | ||
"Unsupported interval argument. Expected string literal, got: {value:?}" | ||
); | ||
} | ||
if let SQLExpr::BinaryOp { left, op, right } = *interval.value { | ||
let df_op = match op { | ||
BinaryOperator::Plus => Operator::Plus, | ||
BinaryOperator::Minus => Operator::Minus, | ||
_ => { | ||
return not_impl_err!("Unsupported interval operator: {op:?}"); | ||
} | ||
} | ||
_ => { | ||
return not_impl_err!( | ||
"Unsupported interval argument. Expected string literal, got: {:?}", | ||
interval.value | ||
); | ||
} | ||
}; | ||
}; | ||
let left_expr = self.sql_interval_to_expr( | ||
negative, | ||
Interval { | ||
value: left, | ||
leading_field: interval.leading_field.clone(), | ||
leading_precision: None, | ||
last_field: None, | ||
fractional_seconds_precision: None, | ||
}, | ||
)?; | ||
let right_expr = self.sql_interval_to_expr( | ||
false, | ||
Interval { | ||
value: right, | ||
leading_field: interval.leading_field, | ||
leading_precision: None, | ||
last_field: None, | ||
fractional_seconds_precision: None, | ||
}, | ||
)?; | ||
return Ok(Expr::BinaryExpr(BinaryExpr::new( | ||
Box::new(left_expr), | ||
df_op, | ||
Box::new(right_expr), | ||
))); | ||
} | ||
|
||
let value = interval_literal(*interval.value, negative)?; | ||
|
||
let value = if has_units(&value) { | ||
// If the interval already contains a unit | ||
|
@@ -343,6 +257,41 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { | |
} | ||
} | ||
|
||
fn interval_literal(interval_value: SQLExpr, negative: bool) -> Result<String> { | ||
let s = match interval_value { | ||
SQLExpr::Value(Value::SingleQuotedString(s) | Value::DoubleQuotedString(s)) => s, | ||
SQLExpr::Value(Value::Number(ref v, long)) => { | ||
if long { | ||
return not_impl_err!( | ||
"Unsupported interval argument. Long number not supported: {interval_value:?}" | ||
); | ||
} else { | ||
v.to_string() | ||
} | ||
} | ||
SQLExpr::UnaryOp { op, expr } => { | ||
let negative = match op { | ||
UnaryOperator::Minus => !negative, | ||
UnaryOperator::Plus => negative, | ||
_ => { | ||
return not_impl_err!( | ||
"Unsupported SQL unary operator in interval {op:?}" | ||
); | ||
} | ||
}; | ||
interval_literal(*expr, negative)? | ||
} | ||
_ => { | ||
return not_impl_err!("Unsupported interval argument. Expected string literal or number, got: {interval_value:?}"); | ||
} | ||
}; | ||
if negative { | ||
Ok(format!("-{s}")) | ||
} else { | ||
Ok(s) | ||
} | ||
} | ||
|
||
// TODO make interval parsing better in arrow-rs / expose `IntervalType` | ||
fn has_units(val: &str) -> bool { | ||
let val = val.to_lowercase(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ use datafusion_sql::unparser::dialect::{ | |
use datafusion_sql::unparser::{expr_to_sql, plan_to_sql, Unparser}; | ||
|
||
use datafusion_functions::core::planner::CoreFunctionPlanner; | ||
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect}; | ||
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect, PostgreSqlDialect}; | ||
use sqlparser::parser::Parser; | ||
|
||
use crate::common::{MockContextProvider, MockSessionState}; | ||
|
@@ -481,11 +481,17 @@ fn test_table_references_in_plan_to_sql() { | |
assert_eq!(format!("{}", sql), expected_sql) | ||
} | ||
|
||
test("catalog.schema.table", "SELECT catalog.\"schema\".\"table\".id, catalog.\"schema\".\"table\".\"value\" FROM catalog.\"schema\".\"table\""); | ||
test("schema.table", "SELECT \"schema\".\"table\".id, \"schema\".\"table\".\"value\" FROM \"schema\".\"table\""); | ||
test( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a driveby (unrelated) cleanup, right? (this is fine I am just verifying my understanding) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was necessary to get tests to pass, I assume it came from some other change in sqlparser, I didn't look too hard. But it's not just cosmetic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this mostly changes |
||
"catalog.schema.table", | ||
r#"SELECT "catalog"."schema"."table".id, "catalog"."schema"."table"."value" FROM "catalog"."schema"."table""#, | ||
); | ||
test( | ||
"schema.table", | ||
r#"SELECT "schema"."table".id, "schema"."table"."value" FROM "schema"."table""#, | ||
); | ||
test( | ||
"table", | ||
"SELECT \"table\".id, \"table\".\"value\" FROM \"table\"", | ||
r#"SELECT "table".id, "table"."value" FROM "table""#, | ||
); | ||
} | ||
|
||
|
@@ -507,10 +513,10 @@ fn test_table_scan_with_no_projection_in_plan_to_sql() { | |
|
||
test( | ||
"catalog.schema.table", | ||
"SELECT * FROM catalog.\"schema\".\"table\"", | ||
r#"SELECT * FROM "catalog"."schema"."table""#, | ||
); | ||
test("schema.table", "SELECT * FROM \"schema\".\"table\""); | ||
test("table", "SELECT * FROM \"table\""); | ||
test("schema.table", r#"SELECT * FROM "schema"."table""#); | ||
test("table", r#"SELECT * FROM "table""#); | ||
} | ||
|
||
#[test] | ||
|
@@ -590,8 +596,8 @@ fn test_pretty_roundtrip() -> Result<()> { | |
Ok(()) | ||
} | ||
|
||
fn sql_round_trip(query: &str, expect: &str) { | ||
let statement = Parser::new(&GenericDialect {}) | ||
fn sql_round_trip(query: &str, expect: &str, dialect: &dyn Dialect) { | ||
let statement = Parser::new(dialect) | ||
.try_with_sql(query) | ||
.unwrap() | ||
.parse_statement() | ||
|
@@ -609,16 +615,36 @@ fn sql_round_trip(query: &str, expect: &str) { | |
|
||
#[test] | ||
fn test_interval_lhs_eq() { | ||
sql_round_trip( | ||
"select interval 2 second = interval 2 second", | ||
"SELECT (INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS' = INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS')", | ||
&GenericDialect {} | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_interval_lhs_eq_pg() { | ||
sql_round_trip( | ||
"select interval '2 seconds' = interval '2 seconds'", | ||
"SELECT (INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS' = INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS')", | ||
&PostgreSqlDialect {} | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_interval_lhs_lt() { | ||
sql_round_trip( | ||
"select interval 2 second < interval 2 second", | ||
"SELECT (INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS' < INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS')", | ||
&GenericDialect {} | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_interval_lhs_lt_pg() { | ||
sql_round_trip( | ||
"select interval '2 seconds' < interval '2 seconds'", | ||
"SELECT (INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS' < INTERVAL '0 YEARS 0 MONS 0 DAYS 0 HOURS 0 MINS 2.000000000 SECS')", | ||
&PostgreSqlDialect {} | ||
); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.