Skip to content
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

fix: Fix sqlite datetime output #1970

Merged
merged 14 commits into from
Mar 1, 2023
10 changes: 9 additions & 1 deletion prql-compiler/src/sql/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use core::fmt::Debug;
use serde::{Deserialize, Serialize};
use std::any::{Any, TypeId};
use strum::{EnumMessage, IntoEnumIterator};

/// SQL dialect.
Expand Down Expand Up @@ -109,7 +110,7 @@ pub(super) enum ColumnExclude {
Except,
}

pub(super) trait DialectHandler {
pub(super) trait DialectHandler: Any {
max-sixty marked this conversation as resolved.
Show resolved Hide resolved
fn use_top(&self) -> bool {
false
}
Expand Down Expand Up @@ -155,6 +156,13 @@ pub(super) trait DialectHandler {
}
}

impl dyn DialectHandler {
#[inline]
pub fn is<T: DialectHandler + 'static>(&self) -> bool {
TypeId::of::<T>() == self.type_id()
}
}

impl DialectHandler for GenericDialect {}

impl DialectHandler for PostgresDialect {
Expand Down
210 changes: 200 additions & 10 deletions prql-compiler/src/sql/gen_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,17 @@ pub(super) fn translate_literal(l: Literal, ctx: &Context) -> Result<sql_ast::Ex
Literal::Boolean(b) => sql_ast::Expr::Value(Value::Boolean(b)),
Literal::Float(f) => sql_ast::Expr::Value(Value::Number(format!("{f:?}"), false)),
Literal::Integer(i) => sql_ast::Expr::Value(Value::Number(format!("{i}"), false)),
Literal::Date(value) => sql_ast::Expr::TypedString {
data_type: sql_ast::DataType::Date,
Literal::Date(value) => translate_datetime_literal(sql_ast::DataType::Date, value, ctx),
Literal::Time(value) => translate_datetime_literal(
sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None),
value,
},
Literal::Time(value) => sql_ast::Expr::TypedString {
data_type: sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None),
value,
},
Literal::Timestamp(value) => sql_ast::Expr::TypedString {
data_type: sql_ast::DataType::Timestamp(None, sql_ast::TimezoneInfo::None),
ctx,
),
Literal::Timestamp(value) => translate_datetime_literal(
sql_ast::DataType::Timestamp(None, sql_ast::TimezoneInfo::None),
value,
},
ctx,
),
Literal::ValueAndUnit(vau) => {
let sql_parser_datetime = match vau.unit.as_str() {
"years" => DateTimeField::Year,
Expand Down Expand Up @@ -184,6 +183,60 @@ pub(super) fn translate_literal(l: Literal, ctx: &Context) -> Result<sql_ast::Ex
})
}

fn translate_datetime_literal(
max-sixty marked this conversation as resolved.
Show resolved Hide resolved
data_type: sql_ast::DataType,
value: String,
ctx: &Context,
) -> sql_ast::Expr {
if ctx.dialect.is::<crate::sql::dialect::SQLiteDialect>() {
max-sixty marked this conversation as resolved.
Show resolved Hide resolved
translate_datetime_literal_with_sqlite_function(data_type, value)
} else {
translate_dateatime_literal_with_typed_string(data_type, value)
bcho marked this conversation as resolved.
Show resolved Hide resolved
}
}

fn translate_dateatime_literal_with_typed_string(
bcho marked this conversation as resolved.
Show resolved Hide resolved
data_type: sql_ast::DataType,
value: String,
) -> sql_ast::Expr {
sql_ast::Expr::TypedString { data_type, value }
}

fn translate_datetime_literal_with_sqlite_function(
data_type: sql_ast::DataType,
value: String,
) -> sql_ast::Expr {
let timezone_indicator_regex = Regex::new(r"([+-]\d{2}):?(\d{2})$").unwrap();
bcho marked this conversation as resolved.
Show resolved Hide resolved
let time_value = if let Some(groups) = timezone_indicator_regex.captures(value.as_str()) {
// formalize the timezone indicator to be [+-]HH:MM
// ref: https://www.sqlite.org/lang_datefunc.html
timezone_indicator_regex
.replace(&value, format!("{}:{}", &groups[1], &groups[2]).as_str())
.to_string()
} else {
value
};

let arg = FunctionArg::Unnamed(FunctionArgExpr::Expr(sql_ast::Expr::Value(
Value::SingleQuotedString(time_value),
)));

let func_name = match data_type {
sql_ast::DataType::Date => data_type.to_string(),
sql_ast::DataType::Time(..) => data_type.to_string(),
sql_ast::DataType::Timestamp(..) => "DATETIME".to_string(),
_ => unreachable!(),
};

sql_ast::Expr::Function(Function {
name: ObjectName(vec![sql_ast::Ident::new(func_name)]),
args: vec![arg],
over: None,
distinct: false,
special: false,
})
}

pub(super) fn translate_cid(cid: CId, ctx: &mut Context) -> Result<sql_ast::Expr> {
if ctx.query.pre_projection {
log::debug!("translating {cid:?} pre projection");
Expand Down Expand Up @@ -930,4 +983,141 @@ mod test {

Ok(())
}

#[test]
fn test_translate_datetime_literal_with_sqlite_function() -> Result<()> {
assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Date,
"2020-01-01".to_string(),
),
@r###"
---
Function:
name:
- value: DATE
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: 2020-01-01
over: ~
distinct: false
special: false
"###
);

assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None),
"03:05".to_string(),
),
@r###"
---
Function:
name:
- value: TIME
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: "03:05"
over: ~
distinct: false
special: false
"###
);

assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None),
"03:05+08:00".to_string(),
),
@r###"
---
Function:
name:
- value: TIME
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: "03:05+08:00"
over: ~
distinct: false
special: false
"###
);

assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None),
"03:05+0800".to_string(),
),
@r###"
---
Function:
name:
- value: TIME
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: "03:05+08:00"
over: ~
distinct: false
special: false
"###
);

assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Timestamp(None, sql_ast::TimezoneInfo::None),
"2021-03-14T03:05+0800".to_string(),
),
@r###"
---
Function:
name:
- value: DATETIME
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: "2021-03-14T03:05+08:00"
over: ~
distinct: false
special: false
"###
);

assert_yaml_snapshot!(
translate_datetime_literal_with_sqlite_function(
sql_ast::DataType::Timestamp(None, sql_ast::TimezoneInfo::None),
"2021-03-14T03:05+08:00".to_string(),
),
@r###"
---
Function:
name:
- value: DATETIME
quote_style: ~
args:
- Unnamed:
Expr:
Value:
SingleQuotedString: "2021-03-14T03:05+08:00"
over: ~
distinct: false
special: false
"###
);

Ok(())
}
}
42 changes: 42 additions & 0 deletions prql-compiler/src/sql/gen_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,4 +846,46 @@ mod test {
_expr_0 > 3
"###);
}

#[test]
bcho marked this conversation as resolved.
Show resolved Hide resolved
fn test_datetime() {
let query = &r#"
from test_table
select [date = @2022-12-31, time = @08:30, timestamp = @2020-01-01T13:19:55-0800]
"#;

assert_snapshot!(
crate::test::compile(query).unwrap(),
@r###"SELECT
DATE '2022-12-31' AS date,
TIME '08:30' AS time,
TIMESTAMP '2020-01-01T13:19:55-0800' AS timestamp
FROM
test_table
"###
)
}

#[test]
fn test_datetime_sqlite() {
let query = &r#"
from test_table
select [date = @2022-12-31, time = @08:30, timestamp = @2020-01-01T13:19:55-0800]
"#;

let opts = crate::Options::default()
.no_signature()
.with_target(crate::Target::Sql(Some(crate::sql::Dialect::SQLite)));

assert_snapshot!(
crate::compile(query, &opts).unwrap(),
@r###"SELECT
DATE('2022-12-31') AS date,
TIME('08:30') AS time,
DATETIME('2020-01-01T13:19:55-08:00') AS timestamp
FROM
test_table
"###
);
}
}