From 8691eadc3014739a528f88c95d25ee8ea931ca60 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Wed, 17 Dec 2025 09:50:03 -0600 Subject: [PATCH 1/8] Databricks `timestamp as of` syntax support --- src/ast/query.rs | 5 +++ src/dialect/databricks.rs | 5 +++ src/parser/mod.rs | 5 +++ tests/sqlparser_bigquery.rs | 6 ++- tests/sqlparser_databricks.rs | 84 +++++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 16fc9ec0e..efec56ffd 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 c5d5f9740..01f5d1ede 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 54fb32737..d4ad931c2 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 24b9efcaa..6afac918f 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 065e8f9e7..8f3385eea 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()); +} From bb92c8beb6143152b3b8cf920aecb18a7eb486b3 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 05:22:54 -0600 Subject: [PATCH 2/8] Doc comment Co-authored-by: Ifeanyi Ubah --- src/dialect/databricks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 01f5d1ede..1ca0d162c 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -47,7 +47,7 @@ impl Dialect for DatabricksDialect { true } - // https://docs.databricks.com/gcp/en/delta/history#delta-time-travel-syntax + /// https://docs.databricks.com/gcp/en/delta/history#delta-time-travel-syntax fn supports_timestamp_versioning(&self) -> bool { true } From 324ade9afc7c88778cb530528bec3d335de88f71 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 05:24:02 -0600 Subject: [PATCH 3/8] Remove redundant comments --- tests/sqlparser_bigquery.rs | 1 - tests/sqlparser_databricks.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 6afac918f..e31fd4108 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1754,7 +1754,6 @@ 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()); } diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 8f3385eea..31cfbe16a 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -440,7 +440,6 @@ fn parse_table_time_travel() { 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()); From b3534992a082e3c5d13129862843c60b8c42627a Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 07:15:33 -0600 Subject: [PATCH 4/8] Remove databricks dialect guard --- src/parser/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 93e6a7345..ab6246930 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -15358,9 +15358,7 @@ 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]) - { + } else if self.parse_keywords(&[Keyword::TIMESTAMP, Keyword::AS, Keyword::OF]) { let expr = self.parse_expr()?; return Ok(Some(TableVersion::TimestampAsOf(expr))); } From 759406fc535ca722543d04b545194b477c3c3c98 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 07:15:50 -0600 Subject: [PATCH 5/8] Valid Snowflake in negative case tests --- tests/sqlparser_databricks.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 31cfbe16a..a60d3b7d7 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -393,7 +393,8 @@ fn parse_table_time_travel() { },] ); - let sql = "SELECT 1 FROM t1 TIMESTAMP AS OF CURRENT_TIMESTAMP() - INTERVAL 12 HOURS".to_string(); + 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, @@ -443,9 +444,9 @@ fn parse_table_time_travel() { 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(); + let sql = "SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')".to_string(); assert!(databricks().parse_sql_statements(&sql).is_err()); - let sql = "SELECT 1 FROM t1 BEFORE '{version}'".to_string(); + let sql = "SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')".to_string(); assert!(databricks().parse_sql_statements(&sql).is_err()); } From 21783cc67360cc4e0a53f91cd04349b668fcbc33 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 15:48:07 -0600 Subject: [PATCH 6/8] Remove incorrect negative case BQ test --- tests/sqlparser_bigquery.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e31fd4108..bf3d8086f 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -1753,9 +1753,6 @@ 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()); - - let sql = "SELECT 1 FROM t1 TIMESTAMP AS OF '{version}'".to_string(); - assert!(bigquery().parse_sql_statements(&sql).is_err()); } #[test] From 270c41b9ac6c46b396dc47ec435f1a37afe6e883 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Thu, 18 Dec 2025 15:48:49 -0600 Subject: [PATCH 7/8] All dialects should parse TIMESTAMP AS OF --- tests/sqlparser_databricks.rs | 84 +++-------------------------------- 1 file changed, 7 insertions(+), 77 deletions(-) diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index a60d3b7d7..9a9a73fe6 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -369,84 +369,14 @@ fn data_type_timestamp_ntz() { #[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![] - },] - ); + all_dialects_where(|d| d.supports_timestamp_versioning()) + .verified_only_select("SELECT 1 FROM t1 TIMESTAMP AS OF '2018-10-18T22:15:12.013Z'"); - 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![] - },] + all_dialects_where(|d| d.supports_timestamp_versioning()).verified_only_select( + "SELECT 1 FROM t1 TIMESTAMP AS OF CURRENT_TIMESTAMP() - INTERVAL 12 HOURS", ); - let sql = "SELECT 1 FROM t1 FOR TIMESTAMP AS OF 'some_timestamp'".to_string(); - assert!(databricks().parse_sql_statements(&sql).is_err()); - - let sql = "SELECT 1 FROM t1 FOR TIMESTAMP AS OF '{version}'".to_string(); - assert!(databricks().parse_sql_statements(&sql).is_err()); - - let sql = "SELECT * FROM tbl AT(TIMESTAMP => '2024-12-15 00:00:00')".to_string(); - assert!(databricks().parse_sql_statements(&sql).is_err()); - - let sql = "SELECT * FROM tbl BEFORE(TIMESTAMP => '2024-12-15 00:00:00')".to_string(); - assert!(databricks().parse_sql_statements(&sql).is_err()); + assert!(databricks() + .parse_sql_statements("SELECT 1 FROM t1 FOR TIMESTAMP AS OF 'some_timestamp'") + .is_err()); } From 8cabd949e951a83e48c20dbace83727d46e7c690 Mon Sep 17 00:00:00 2001 From: James Vorderbruggen Date: Fri, 19 Dec 2025 06:08:02 -0600 Subject: [PATCH 8/8] Fix docstring --- src/dialect/databricks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 1ca0d162c..ec866295d 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -47,7 +47,7 @@ impl Dialect for DatabricksDialect { true } - /// https://docs.databricks.com/gcp/en/delta/history#delta-time-travel-syntax + /// fn supports_timestamp_versioning(&self) -> bool { true }