diff --git a/src/ast/query.rs b/src/ast/query.rs index 16fc9ec0..efec56ff 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2241,6 +2241,10 @@ pub enum TableVersion { /// When the table version is defined using `FOR SYSTEM_TIME AS OF`. /// For example: `SELECT * FROM tbl FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)` ForSystemTimeAsOf(Expr), + /// When the table version is defined using `TIMESTAMP AS OF`. + /// Databricks supports this syntax. + /// For example: `SELECT * FROM tbl TIMESTAMP AS OF CURRENT_TIMESTAMP() - INTERVAL 1 HOUR` + TimestampAsOf(Expr), /// When the table version is defined using a function. /// For example: `SELECT * FROM tbl AT(TIMESTAMP => '2020-08-14 09:30:00')` Function(Expr), @@ -2250,6 +2254,7 @@ impl Display for TableVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { TableVersion::ForSystemTimeAsOf(e) => write!(f, "FOR SYSTEM_TIME AS OF {e}")?, + TableVersion::TimestampAsOf(e) => write!(f, "TIMESTAMP AS OF {e}")?, TableVersion::Function(func) => write!(f, "{func}")?, } Ok(()) diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index c5d5f974..01f5d1ed 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -47,6 +47,11 @@ impl Dialect for DatabricksDialect { true } + // https://docs.databricks.com/gcp/en/delta/history#delta-time-travel-syntax + fn supports_timestamp_versioning(&self) -> bool { + true + } + fn supports_lambda_functions(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 54fb3273..d4ad931c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15137,6 +15137,11 @@ impl<'a> Parser<'a> { let func_name = self.parse_object_name(true)?; let func = self.parse_function(func_name)?; return Ok(Some(TableVersion::Function(func))); + } else if dialect_of!(self is DatabricksDialect) + && self.parse_keywords(&[Keyword::TIMESTAMP, Keyword::AS, Keyword::OF]) + { + let expr = self.parse_expr()?; + return Ok(Some(TableVersion::TimestampAsOf(expr))); } } Ok(None) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 24b9efca..6afac918 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1739,7 +1739,7 @@ fn parse_table_time_travel() { args: None, with_hints: vec![], version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( - Value::SingleQuotedString(version).with_empty_span() + Value::SingleQuotedString(version.clone()).with_empty_span() ))), partitions: vec![], with_ordinality: false, @@ -1753,6 +1753,10 @@ fn parse_table_time_travel() { let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); assert!(bigquery().parse_sql_statements(&sql).is_err()); + + // The following time travel syntax(es) are invalid in BigQuery dialect + let sql = "SELECT 1 FROM t1 TIMESTAMP AS OF '{version}'".to_string(); + assert!(bigquery().parse_sql_statements(&sql).is_err()); } #[test] diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 065e8f9e..8f3385ee 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -366,3 +366,87 @@ fn data_type_timestamp_ntz() { s => panic!("Unexpected statement: {s:?}"), } } + +#[test] +fn parse_table_time_travel() { + let version = "2018-10-18T22:15:12.013Z".to_string(); + let sql = format!("SELECT 1 FROM t1 TIMESTAMP AS OF '{version}'"); + let select = databricks().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::TimestampAsOf(Expr::Value( + Value::SingleQuotedString(version.clone()).with_empty_span() + ))), + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 TIMESTAMP AS OF CURRENT_TIMESTAMP() - INTERVAL 12 HOURS".to_string(); + let select = databricks().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName::from(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::TimestampAsOf(Expr::BinaryOp { + left: Box::new(Expr::Function(Function { + name: ObjectName::from(vec![Ident::new("CURRENT_TIMESTAMP")]), + uses_odbc_syntax: false, + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + })), + op: BinaryOperator::Minus, + right: Box::new(Expr::Interval(Interval { + value: Box::new(Expr::Value(number("12").into())), + leading_field: Some(DateTimeField::Hours), + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })) + })), + partitions: vec![], + with_ordinality: false, + json_path: None, + sample: None, + index_hints: vec![], + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 FOR TIMESTAMP AS OF 'some_timestamp'".to_string(); + assert!(databricks().parse_sql_statements(&sql).is_err()); + + // The following time travel syntax(es) are invalid in Databricks dialect + let sql = "SELECT 1 FROM t1 FOR TIMESTAMP AS OF '{version}'".to_string(); + assert!(databricks().parse_sql_statements(&sql).is_err()); + + let sql = "SELECT 1 FROM t1 AT '{version}'".to_string(); + assert!(databricks().parse_sql_statements(&sql).is_err()); + + let sql = "SELECT 1 FROM t1 BEFORE '{version}'".to_string(); + assert!(databricks().parse_sql_statements(&sql).is_err()); +}