Skip to content

feat: support datetime_field as expr for bigquery #1971

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,9 @@ pub enum Expr {
syntax: ExtractSyntax,
expr: Box<Expr>,
},
/// In BigQuery, the `DATE_TRUNC` and `DATETIME_TRUNC` functions can be used to truncate a timestamp to a different granularity.
/// e.g. `DATE_TRUNC(CURRENT_DATE(), DAY)`
DateTimeField(DateTimeField),
/// ```sql
/// CEIL(<expr> [TO DateTimeField])
/// ```
Expand Down Expand Up @@ -1937,6 +1940,7 @@ impl fmt::Display for Expr {
Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
Expr::Lambda(lambda) => write!(f, "{lambda}"),
Expr::MemberOf(member_of) => write!(f, "{member_of}"),
Expr::DateTimeField(field) => write!(f, "{field}"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1625,6 +1625,7 @@ impl Spanned for Expr {
Expr::Prior(expr) => expr.span(),
Expr::Lambda(_) => Span::empty(),
Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()),
Expr::DateTimeField(_) => Span::empty(),
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14896,6 +14896,12 @@ impl<'a> Parser<'a> {
}

pub fn parse_function_args(&mut self) -> Result<FunctionArg, ParserError> {
if dialect_of!(self is BigQueryDialect) && self.next_token_is_temporal_unit() {
let unit = self.parse_date_time_field()?;
return Ok(FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::DateTimeField(unit),
)));
}
let arg = if self.dialect.supports_named_fn_args_with_expr_name() {
self.maybe_parse(|p| {
let name = p.parse_expr()?;
Expand Down
98 changes: 98 additions & 0 deletions tests/sqlparser_bigquery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2566,3 +2566,101 @@ fn test_struct_trailing_and_nested_bracket() {
)
);
}

#[test]
fn test_datetime_granularity() {
let stmt = bigquery().verified_stmt(concat!(
"SELECT ",
"DATE_TRUNC(CURRENT_DATE, DAY), ",
"DATE_TRUNC(CURRENT_DATE, WEEK(MONDAY)) ",
"FROM my_table",
));
Comment on lines +2572 to +2577
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

double checking the intent of the changes: my understanding is that currently given SELECT DATE_TRUNC(CURRENT_DATE, WEEK(MONDAY), WEEK(MONDAY) should be parsed as a function with name WEEK and having a single identifier MONDAY as argument? Is that indeed the case (vs e.g the parser not being able to handle such a statement)

Copy link
Contributor Author

@chenkovsky chenkovsky Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently, DAY will be parsed as ident, week will be parsed as function. I think this is flawed.

  1. when transpiling bigquery sql to another dialect, DATE_TRUNC(CURRENT_DATE, DAY) should be transpiled to DATE_TRUNC(CURRENT_DATE,'day'), but currently, it will be transpiled into DATE_TRUNC(CURRENT_DATE, DAY).
  2. when compiling sql to logical plan, planner will try to find column called DAY, MONDAY, and function called WEEK. we should tell planner this is not function or column. I think we should convert it into str when planning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, I think the current behavior of having it parsed as a function is preferable since the construct is syntatically a function - there's quite a lot of such special scenarios across dialects and it would likely be unwieldy to cover them consistently and I think out of scope for the parser as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks. if it's out of scope, i will close this pr. it can also be solved with a postprocessor.

match stmt {
Statement::Query(query) => {
let body = query.body.as_ref();
match body {
SetExpr::Select(select) => {
let projection = &select.projection;
assert_eq!(
projection[0],
SelectItem::UnnamedExpr(Expr::Function(Function {
name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new(
"DATE_TRUNC"
))]),
args: FunctionArguments::List(FunctionArgumentList {
args: vec![
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Function(
Function {
name: ObjectName(vec![ObjectNamePart::Identifier(
Ident::new("CURRENT_DATE")
)]),
args: FunctionArguments::None,
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
}
))),
FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::DateTimeField(DateTimeField::Day)
)),
],
clauses: vec![],
duplicate_treatment: None,
}),
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
}))
);
assert_eq!(
projection[1],
SelectItem::UnnamedExpr(Expr::Function(Function {
name: ObjectName(vec![ObjectNamePart::Identifier(Ident::new(
"DATE_TRUNC"
))]),
args: FunctionArguments::List(FunctionArgumentList {
args: vec![
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Function(
Function {
name: ObjectName(vec![ObjectNamePart::Identifier(
Ident::new("CURRENT_DATE")
)]),
args: FunctionArguments::None,
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
}
))),
FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::DateTimeField(DateTimeField::Week(Some(Ident::new(
"MONDAY"
))))
)),
],
clauses: vec![],
duplicate_treatment: None,
}),
uses_odbc_syntax: false,
parameters: FunctionArguments::None,
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
}))
);
}
_ => panic!("Expected a select statement"),
}
}
_ => panic!("Expected a select statement"),
}
}
Loading