Skip to content

Commit d2f6faf

Browse files
Omit NULLS FIRST/LAST when unparsing ORDER BY clauses for MySQL (#10625)
* Omit `NULLS FIRST/LAST` when deparsing ORDER BY clauses for MySQL * Add test and fix sort_to_sql * Fix link for cargo doc * Remove reference to sqlparser-rs PR
1 parent 529d2c0 commit d2f6faf

File tree

4 files changed

+80
-4
lines changed

4 files changed

+80
-4
lines changed

datafusion/sql/src/unparser/dialect.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,19 @@ use sqlparser::keywords::ALL_KEYWORDS;
2222
///
2323
/// The default dialect tries to avoid quoting identifiers unless necessary (e.g. `a` instead of `"a"`)
2424
/// but this behavior can be overridden as needed
25-
/// Note: this trait will eventually be replaced by the Dialect in the SQLparser package
25+
///
26+
/// **Note**: This trait will eventually be replaced by the Dialect in the SQLparser package
2627
///
2728
/// See <https://github.com/sqlparser-rs/sqlparser-rs/pull/1170>
29+
/// See also the discussion in <https://github.com/apache/datafusion/pull/10625>
2830
pub trait Dialect {
31+
/// Return the character used to quote identifiers.
2932
fn identifier_quote_style(&self, _identifier: &str) -> Option<char>;
33+
34+
/// Does the dialect support specifying `NULLS FIRST/LAST` in `ORDER BY` clauses?
35+
fn supports_nulls_first_in_sort(&self) -> bool {
36+
true
37+
}
3038
}
3139
pub struct DefaultDialect {}
3240

@@ -57,6 +65,10 @@ impl Dialect for MySqlDialect {
5765
fn identifier_quote_style(&self, _: &str) -> Option<char> {
5866
Some('`')
5967
}
68+
69+
fn supports_nulls_first_in_sort(&self) -> bool {
70+
false
71+
}
6072
}
6173

6274
pub struct SqliteDialect {}

datafusion/sql/src/unparser/expr.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,10 +474,17 @@ impl Unparser<'_> {
474474
nulls_first,
475475
}) => {
476476
let sql_parser_expr = self.expr_to_sql(expr)?;
477+
478+
let nulls_first = if self.dialect.supports_nulls_first_in_sort() {
479+
Some(*nulls_first)
480+
} else {
481+
None
482+
};
483+
477484
Ok(Unparsed::OrderByExpr(ast::OrderByExpr {
478485
expr: sql_parser_expr,
479486
asc: Some(*asc),
480-
nulls_first: Some(*nulls_first),
487+
nulls_first,
481488
}))
482489
}
483490
_ => {

datafusion/sql/src/unparser/plan.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,17 @@ impl Unparser<'_> {
439439
.map(|expr: &Expr| match expr {
440440
Expr::Sort(sort_expr) => {
441441
let col = self.expr_to_sql(&sort_expr.expr)?;
442+
443+
let nulls_first = if self.dialect.supports_nulls_first_in_sort() {
444+
Some(sort_expr.nulls_first)
445+
} else {
446+
None
447+
};
448+
442449
Ok(ast::OrderByExpr {
443450
asc: Some(sort_expr.asc),
444451
expr: col,
445-
nulls_first: Some(sort_expr.nulls_first),
452+
nulls_first,
446453
})
447454
}
448455
_ => plan_err!("Expecting Sort expr"),

datafusion/sql/tests/sql_integration.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ use datafusion_expr::{
3333
Volatility, WindowUDF,
3434
};
3535
use datafusion_functions::{string, unicode};
36-
use datafusion_sql::unparser::{expr_to_sql, plan_to_sql};
36+
use datafusion_sql::unparser::dialect::{
37+
DefaultDialect as UnparserDefaultDialect, Dialect as UnparserDialect,
38+
MySqlDialect as UnparserMySqlDialect,
39+
};
40+
use datafusion_sql::unparser::{expr_to_sql, plan_to_sql, Unparser};
3741
use datafusion_sql::{
3842
parser::DFParser,
3943
planner::{ContextProvider, ParserOptions, PlannerContext, SqlToRel},
@@ -4726,6 +4730,52 @@ fn roundtrip_crossjoin() -> Result<()> {
47264730
Ok(())
47274731
}
47284732

4733+
#[test]
4734+
fn roundtrip_statement_with_dialect() -> Result<()> {
4735+
struct TestStatementWithDialect {
4736+
sql: &'static str,
4737+
expected: &'static str,
4738+
parser_dialect: Box<dyn Dialect>,
4739+
unparser_dialect: Box<dyn UnparserDialect>,
4740+
}
4741+
let tests: Vec<TestStatementWithDialect> = vec![
4742+
TestStatementWithDialect {
4743+
sql: "select ta.j1_id from j1 ta order by j1_id limit 10;",
4744+
expected:
4745+
"SELECT `ta`.`j1_id` FROM `j1` AS `ta` ORDER BY `ta`.`j1_id` ASC LIMIT 10",
4746+
parser_dialect: Box::new(MySqlDialect {}),
4747+
unparser_dialect: Box::new(UnparserMySqlDialect {}),
4748+
},
4749+
TestStatementWithDialect {
4750+
sql: "select ta.j1_id from j1 ta order by j1_id limit 10;",
4751+
expected: r#"SELECT ta.j1_id FROM j1 AS ta ORDER BY ta.j1_id ASC NULLS LAST LIMIT 10"#,
4752+
parser_dialect: Box::new(GenericDialect {}),
4753+
unparser_dialect: Box::new(UnparserDefaultDialect {}),
4754+
},
4755+
];
4756+
4757+
for query in tests {
4758+
let statement = Parser::new(&*query.parser_dialect)
4759+
.try_with_sql(query.sql)?
4760+
.parse_statement()?;
4761+
4762+
let context = MockContextProvider::default();
4763+
let sql_to_rel = SqlToRel::new(&context);
4764+
let plan = sql_to_rel.sql_statement_to_plan(statement).unwrap();
4765+
4766+
let unparser = Unparser::new(&*query.unparser_dialect);
4767+
let roundtrip_statement = unparser.plan_to_sql(&plan)?;
4768+
4769+
let actual = format!("{}", &roundtrip_statement);
4770+
println!("roundtrip sql: {actual}");
4771+
println!("plan {}", plan.display_indent());
4772+
4773+
assert_eq!(query.expected, actual);
4774+
}
4775+
4776+
Ok(())
4777+
}
4778+
47294779
#[test]
47304780
fn test_unnest_logical_plan() -> Result<()> {
47314781
let query = "select unnest(struct_col), unnest(array_col), struct_col, array_col from unnest_table";

0 commit comments

Comments
 (0)