Skip to content

Commit d0e7842

Browse files
peaseephillipleblanc
authored and
Nirnay Roy
committed
fix: Rewrite date_trunc and from_unixtime for the SQLite unparser (apache#15630)
* feat: Support date_trunc and from_unixtime in SQLite unparser * feat: Add tests * refactor: Update datafusion/sql/src/unparser/expr.rs Co-authored-by: Phillip LeBlanc <[email protected]> * chore: Cargo fmt * chore: Use import to format properly --------- Co-authored-by: Phillip LeBlanc <[email protected]>
1 parent 07bce13 commit d0e7842

File tree

3 files changed

+187
-2
lines changed

3 files changed

+187
-2
lines changed

datafusion/sql/src/unparser/dialect.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
use std::{collections::HashMap, sync::Arc};
1919

20-
use super::{utils::character_length_to_sql, utils::date_part_to_sql, Unparser};
20+
use super::{
21+
utils::character_length_to_sql, utils::date_part_to_sql,
22+
utils::sqlite_date_trunc_to_sql, utils::sqlite_from_unixtime_to_sql, Unparser,
23+
};
2124
use arrow::datatypes::TimeUnit;
2225
use datafusion_common::Result;
2326
use datafusion_expr::Expr;
@@ -490,6 +493,8 @@ impl Dialect for SqliteDialect {
490493
"character_length" => {
491494
character_length_to_sql(unparser, self.character_length_style(), args)
492495
}
496+
"from_unixtime" => sqlite_from_unixtime_to_sql(unparser, args),
497+
"date_trunc" => sqlite_date_trunc_to_sql(unparser, args),
493498
_ => Ok(None),
494499
}
495500
}

datafusion/sql/src/unparser/expr.rs

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,7 @@ mod tests {
16891689
use std::ops::{Add, Sub};
16901690
use std::{any::Any, sync::Arc, vec};
16911691

1692+
use crate::unparser::dialect::SqliteDialect;
16921693
use arrow::array::{LargeListArray, ListArray};
16931694
use arrow::datatypes::{DataType::Int8, Field, Int32Type, Schema, TimeUnit};
16941695
use ast::ObjectName;
@@ -1701,6 +1702,7 @@ mod tests {
17011702
ScalarUDFImpl, Signature, Volatility, WindowFrame, WindowFunctionDefinition,
17021703
};
17031704
use datafusion_expr::{interval_month_day_nano_lit, ExprFunctionExt};
1705+
use datafusion_functions::datetime::from_unixtime::FromUnixtimeFunc;
17041706
use datafusion_functions::expr_fn::{get_field, named_struct};
17051707
use datafusion_functions_aggregate::count::count_udaf;
17061708
use datafusion_functions_aggregate::expr_fn::sum;
@@ -1712,7 +1714,7 @@ mod tests {
17121714

17131715
use crate::unparser::dialect::{
17141716
CharacterLengthStyle, CustomDialect, CustomDialectBuilder, DateFieldExtractStyle,
1715-
Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler,
1717+
DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler,
17161718
};
17171719

17181720
use super::*;
@@ -2871,6 +2873,115 @@ mod tests {
28712873
Ok(())
28722874
}
28732875

2876+
#[test]
2877+
fn test_from_unixtime() -> Result<()> {
2878+
let default_dialect: Arc<dyn Dialect> = Arc::new(DefaultDialect {});
2879+
let sqlite_dialect: Arc<dyn Dialect> = Arc::new(SqliteDialect {});
2880+
2881+
for (dialect, expected) in [
2882+
(default_dialect, "from_unixtime(date_col)"),
2883+
(sqlite_dialect, "datetime(`date_col`, 'unixepoch')"),
2884+
] {
2885+
let unparser = Unparser::new(dialect.as_ref());
2886+
let expr = Expr::ScalarFunction(ScalarFunction {
2887+
func: Arc::new(ScalarUDF::from(FromUnixtimeFunc::new())),
2888+
args: vec![col("date_col")],
2889+
});
2890+
2891+
let ast = unparser.expr_to_sql(&expr)?;
2892+
2893+
let actual = ast.to_string();
2894+
let expected = expected.to_string();
2895+
2896+
assert_eq!(actual, expected);
2897+
}
2898+
Ok(())
2899+
}
2900+
2901+
#[test]
2902+
fn test_date_trunc() -> Result<()> {
2903+
let default_dialect: Arc<dyn Dialect> = Arc::new(DefaultDialect {});
2904+
let sqlite_dialect: Arc<dyn Dialect> = Arc::new(SqliteDialect {});
2905+
2906+
for (dialect, precision, expected) in [
2907+
(
2908+
Arc::clone(&default_dialect),
2909+
"YEAR",
2910+
"date_trunc('YEAR', date_col)",
2911+
),
2912+
(
2913+
Arc::clone(&sqlite_dialect),
2914+
"YEAR",
2915+
"strftime('%Y', `date_col`)",
2916+
),
2917+
(
2918+
Arc::clone(&default_dialect),
2919+
"MONTH",
2920+
"date_trunc('MONTH', date_col)",
2921+
),
2922+
(
2923+
Arc::clone(&sqlite_dialect),
2924+
"MONTH",
2925+
"strftime('%Y-%m', `date_col`)",
2926+
),
2927+
(
2928+
Arc::clone(&default_dialect),
2929+
"DAY",
2930+
"date_trunc('DAY', date_col)",
2931+
),
2932+
(
2933+
Arc::clone(&sqlite_dialect),
2934+
"DAY",
2935+
"strftime('%Y-%m-%d', `date_col`)",
2936+
),
2937+
(
2938+
Arc::clone(&default_dialect),
2939+
"HOUR",
2940+
"date_trunc('HOUR', date_col)",
2941+
),
2942+
(
2943+
Arc::clone(&sqlite_dialect),
2944+
"HOUR",
2945+
"strftime('%Y-%m-%d %H', `date_col`)",
2946+
),
2947+
(
2948+
Arc::clone(&default_dialect),
2949+
"MINUTE",
2950+
"date_trunc('MINUTE', date_col)",
2951+
),
2952+
(
2953+
Arc::clone(&sqlite_dialect),
2954+
"MINUTE",
2955+
"strftime('%Y-%m-%d %H:%M', `date_col`)",
2956+
),
2957+
(default_dialect, "SECOND", "date_trunc('SECOND', date_col)"),
2958+
(
2959+
sqlite_dialect,
2960+
"SECOND",
2961+
"strftime('%Y-%m-%d %H:%M:%S', `date_col`)",
2962+
),
2963+
] {
2964+
let unparser = Unparser::new(dialect.as_ref());
2965+
let expr = Expr::ScalarFunction(ScalarFunction {
2966+
func: Arc::new(ScalarUDF::from(
2967+
datafusion_functions::datetime::date_trunc::DateTruncFunc::new(),
2968+
)),
2969+
args: vec![
2970+
Expr::Literal(ScalarValue::Utf8(Some(precision.to_string()))),
2971+
col("date_col"),
2972+
],
2973+
});
2974+
2975+
let ast = unparser.expr_to_sql(&expr)?;
2976+
2977+
let actual = ast.to_string();
2978+
let expected = expected.to_string();
2979+
2980+
assert_eq!(actual, expected);
2981+
}
2982+
Ok(())
2983+
}
2984+
28742985
#[test]
28752986
fn test_dictionary_to_sql() -> Result<()> {
28762987
let dialect = CustomDialectBuilder::new().build();

datafusion/sql/src/unparser/utils.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,72 @@ pub(crate) fn character_length_to_sql(
500500
character_length_args,
501501
)?))
502502
}
503+
504+
/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc.
505+
/// This remaps `from_unixtime` to `datetime(expr, 'unixepoch')`, expecting the input to be in seconds.
506+
/// It supports no other arguments, so if any are supplied it will return an error.
507+
///
508+
/// # Errors
509+
///
510+
/// - If the number of arguments is not 1 - the column or expression to convert.
511+
/// - If the scalar function cannot be converted to SQL.
512+
pub(crate) fn sqlite_from_unixtime_to_sql(
513+
unparser: &Unparser,
514+
from_unixtime_args: &[Expr],
515+
) -> Result<Option<ast::Expr>> {
516+
if from_unixtime_args.len() != 1 {
517+
return internal_err!(
518+
"from_unixtime for SQLite expects 1 argument, found {}",
519+
from_unixtime_args.len()
520+
);
521+
}
522+
523+
Ok(Some(unparser.scalar_function_to_sql(
524+
"datetime",
525+
&[
526+
from_unixtime_args[0].clone(),
527+
Expr::Literal(ScalarValue::Utf8(Some("unixepoch".to_string()))),
528+
],
529+
)?))
530+
}
531+
532+
/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc.
533+
/// This uses the `strftime` function to format the timestamp as a string depending on the truncation unit.
534+
///
535+
/// # Errors
536+
///
537+
/// - If the number of arguments is not 2 - truncation unit and the column or expression to convert.
538+
/// - If the scalar function cannot be converted to SQL.
539+
pub(crate) fn sqlite_date_trunc_to_sql(
540+
unparser: &Unparser,
541+
date_trunc_args: &[Expr],
542+
) -> Result<Option<ast::Expr>> {
543+
if date_trunc_args.len() != 2 {
544+
return internal_err!(
545+
"date_trunc for SQLite expects 2 arguments, found {}",
546+
date_trunc_args.len()
547+
);
548+
}
549+
550+
if let Expr::Literal(ScalarValue::Utf8(Some(unit))) = &date_trunc_args[0] {
551+
let format = match unit.to_lowercase().as_str() {
552+
"year" => "%Y",
553+
"month" => "%Y-%m",
554+
"day" => "%Y-%m-%d",
555+
"hour" => "%Y-%m-%d %H",
556+
"minute" => "%Y-%m-%d %H:%M",
557+
"second" => "%Y-%m-%d %H:%M:%S",
558+
_ => return Ok(None),
559+
};
560+
561+
return Ok(Some(unparser.scalar_function_to_sql(
562+
"strftime",
563+
&[
564+
Expr::Literal(ScalarValue::Utf8(Some(format.to_string()))),
565+
date_trunc_args[1].clone(),
566+
],
567+
)?));
568+
}
569+
570+
Ok(None)
571+
}

0 commit comments

Comments
 (0)