From 57e73c6e7a18e58dad84ba5a303fd0bad38b8dcf Mon Sep 17 00:00:00 2001 From: Marko Grujic Date: Fri, 18 Aug 2023 16:23:48 +0200 Subject: [PATCH 1/2] Add support for temporal querying in BigQuery and MSSQL --- src/ast/mod.rs | 3 +- src/ast/query.rs | 26 +++++++++++ src/ast/visitor.rs | 84 +++++++++++++++++++++++++++++++---- src/parser/mod.rs | 26 +++++++++++ src/test_utils.rs | 1 + tests/sqlparser_bigquery.rs | 24 ++++++++++ tests/sqlparser_clickhouse.rs | 3 ++ tests/sqlparser_common.rs | 29 ++++++++++++ tests/sqlparser_duckdb.rs | 4 ++ tests/sqlparser_hive.rs | 2 + tests/sqlparser_mssql.rs | 28 +++++++++++- tests/sqlparser_mysql.rs | 7 ++- tests/sqlparser_postgres.rs | 2 + tests/sqlparser_redshift.rs | 4 ++ tests/sqlparser_snowflake.rs | 2 + 15 files changed, 233 insertions(+), 12 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 47419a893..7788934c9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -40,7 +40,8 @@ pub use self::query::{ JoinConstraint, JoinOperator, LateralView, LockClause, LockType, NamedWindowDefinition, NonBlock, Offset, OffsetRows, OrderByExpr, Query, RenameSelectItem, ReplaceSelectElement, ReplaceSelectItem, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Table, - TableAlias, TableFactor, TableWithJoins, Top, Values, WildcardAdditionalOptions, With, + TableAlias, TableFactor, TableVersion, TableWithJoins, Top, Values, WildcardAdditionalOptions, + With, }; pub use self::value::{ escape_quoted_string, DateTimeField, DollarQuotedString, TrimWhereField, Value, diff --git a/src/ast/query.rs b/src/ast/query.rs index 5f4c289dc..859e0398b 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -646,6 +646,7 @@ impl fmt::Display for TableWithJoins { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "visitor", visit(with = "visit_table_factor"))] pub enum TableFactor { Table { #[cfg_attr(feature = "visitor", visit(with = "visit_relation"))] @@ -661,6 +662,9 @@ pub enum TableFactor { args: Option>, /// MSSQL-specific `WITH (...)` hints such as NOLOCK. with_hints: Vec, + /// Optional version qualifier to facilitate table time-travel, as + /// supported by BigQuery and MSSQL. + version: Option, }, Derived { lateral: bool, @@ -720,6 +724,7 @@ impl fmt::Display for TableFactor { alias, args, with_hints, + version, } => { write!(f, "{name}")?; if let Some(args) = args { @@ -731,6 +736,9 @@ impl fmt::Display for TableFactor { if !with_hints.is_empty() { write!(f, " WITH ({})", display_comma_separated(with_hints))?; } + if let Some(version) = version { + write!(f, "{version}")?; + } Ok(()) } TableFactor::Derived { @@ -835,6 +843,24 @@ impl fmt::Display for TableAlias { } } +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableVersion { + Timestamp(String), +} + +impl Display for TableVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableVersion::Timestamp(timestamp) => { + write!(f, " FOR SYSTEM_TIME AS OF '{timestamp}'")? + } + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index 8aa038db9..c30222dc2 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -12,7 +12,7 @@ //! Recursive visitors for ast Nodes. See [`Visitor`] for more details. -use crate::ast::{Expr, ObjectName, Statement}; +use crate::ast::{Expr, ObjectName, Statement, TableFactor}; use core::ops::ControlFlow; /// A type that can be visited by a [`Visitor`]. See [`Visitor`] for @@ -115,14 +115,15 @@ visit_noop!(bigdecimal::BigDecimal); /// A visitor that can be used to walk an AST tree. /// -/// `previst_` methods are invoked before visiting all children of the -/// node and `postvisit_` methods are invoked after visiting all +/// `pre_visit_` methods are invoked before visiting all children of the +/// node and `post_visit_` methods are invoked after visiting all /// children of the node. /// /// # See also /// /// These methods provide a more concise way of visiting nodes of a certain type: /// * [visit_relations] +/// * [visit_table_factors] /// * [visit_expressions] /// * [visit_statements] /// @@ -139,7 +140,7 @@ visit_noop!(bigdecimal::BigDecimal); /// } /// /// // Visit relations and exprs before children are visited (depth first walk) -/// // Note you can also visit statements and visit exprs after children have been visitoed +/// // Note you can also visit statements and visit exprs after children have been visited /// impl Visitor for V { /// type Break = (); /// @@ -189,6 +190,16 @@ pub trait Visitor { ControlFlow::Continue(()) } + /// Invoked for any table factors that appear in the AST before visiting children + fn pre_visit_table_factor(&mut self, _table_factor: &TableFactor) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any table factors that appear in the AST after visiting children + fn post_visit_table_factor(&mut self, _table_factor: &TableFactor) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any expressions that appear in the AST before visiting children fn pre_visit_expr(&mut self, _expr: &Expr) -> ControlFlow { ControlFlow::Continue(()) @@ -212,14 +223,15 @@ pub trait Visitor { /// A visitor that can be used to mutate an AST tree. /// -/// `previst_` methods are invoked before visiting all children of the -/// node and `postvisit_` methods are invoked after visiting all +/// `pre_visit_` methods are invoked before visiting all children of the +/// node and `post_visit_` methods are invoked after visiting all /// children of the node. /// /// # See also /// /// These methods provide a more concise way of visiting nodes of a certain type: /// * [visit_relations_mut] +/// * [visit_table_factors_mut] /// * [visit_expressions_mut] /// * [visit_statements_mut] /// @@ -267,6 +279,22 @@ pub trait VisitorMut { ControlFlow::Continue(()) } + /// Invoked for any table factors that appear in the AST before visiting children + fn pre_visit_table_factor( + &mut self, + _table_factor: &mut TableFactor, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + + /// Invoked for any table factors that appear in the AST after visiting children + fn post_visit_table_factor( + &mut self, + _table_factor: &mut TableFactor, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + /// Invoked for any expressions that appear in the AST before visiting children fn pre_visit_expr(&mut self, _expr: &mut Expr) -> ControlFlow { ControlFlow::Continue(()) @@ -609,6 +637,24 @@ mod tests { ControlFlow::Continue(()) } + fn pre_visit_table_factor( + &mut self, + table_factor: &TableFactor, + ) -> ControlFlow { + self.visited + .push(format!("PRE: TABLE FACTOR: {table_factor}")); + ControlFlow::Continue(()) + } + + fn post_visit_table_factor( + &mut self, + table_factor: &TableFactor, + ) -> ControlFlow { + self.visited + .push(format!("POST: TABLE FACTOR: {table_factor}")); + ControlFlow::Continue(()) + } + fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow { self.visited.push(format!("PRE: EXPR: {expr}")); ControlFlow::Continue(()) @@ -647,22 +693,28 @@ mod tests { fn test_sql() { let tests = vec![ ( - "SELECT * from table_name", + "SELECT * from table_name as my_table", vec![ - "PRE: STATEMENT: SELECT * FROM table_name", + "PRE: STATEMENT: SELECT * FROM table_name AS my_table", + "PRE: TABLE FACTOR: table_name AS my_table", "PRE: RELATION: table_name", "POST: RELATION: table_name", - "POST: STATEMENT: SELECT * FROM table_name", + "POST: TABLE FACTOR: table_name AS my_table", + "POST: STATEMENT: SELECT * FROM table_name AS my_table", ], ), ( "SELECT * from t1 join t2 on t1.id = t2.t1_id", vec![ "PRE: STATEMENT: SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "PRE: EXPR: t1.id = t2.t1_id", "PRE: EXPR: t1.id", "POST: EXPR: t1.id", @@ -676,13 +728,17 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2)", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -691,13 +747,17 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2)", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2)", ], @@ -706,16 +766,22 @@ mod tests { "SELECT * from t1 where EXISTS(SELECT column from t2) UNION SELECT * from t3", vec![ "PRE: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", + "PRE: TABLE FACTOR: t1", "PRE: RELATION: t1", "POST: RELATION: t1", + "POST: TABLE FACTOR: t1", "PRE: EXPR: EXISTS (SELECT column FROM t2)", "PRE: EXPR: column", "POST: EXPR: column", + "PRE: TABLE FACTOR: t2", "PRE: RELATION: t2", "POST: RELATION: t2", + "POST: TABLE FACTOR: t2", "POST: EXPR: EXISTS (SELECT column FROM t2)", + "PRE: TABLE FACTOR: t3", "PRE: RELATION: t3", "POST: RELATION: t3", + "POST: TABLE FACTOR: t3", "POST: STATEMENT: SELECT * FROM t1 WHERE EXISTS (SELECT column FROM t2) UNION SELECT * FROM t3", ], ), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 94814627d..98131cedd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6198,6 +6198,9 @@ impl<'a> Parser<'a> { } else { let name = self.parse_object_name()?; + // Parse potential version qualifier + let version = self.parse_table_version()?; + // Postgres, MSSQL: table-valued functions: let args = if self.consume_token(&Token::LParen) { Some(self.parse_optional_args()?) @@ -6228,10 +6231,33 @@ impl<'a> Parser<'a> { alias, args, with_hints, + version, }) } } + /// Parse a given table version specifier. + /// + /// For now it only supports timestamp versioning for BigQuery and MSSQL dialects. + pub fn parse_table_version(&mut self) -> Result, ParserError> { + if dialect_of!(self is BigQueryDialect | MsSqlDialect) + && self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) + { + let timestamp_token = self.next_token(); + match timestamp_token.token { + Token::SingleQuotedString(timestamp) => { + Ok(Some(TableVersion::Timestamp(timestamp))) + } + _ => self.expected( + "Expected Token::SingleQuotedString after FOR SYSTEM_TIME AS OF", + timestamp_token, + ), + } + } else { + Ok(None) + } + } + pub fn parse_derived_table_factor( &mut self, lateral: IsLateral, diff --git a/src/test_utils.rs b/src/test_utils.rs index 0ec595095..91130fb51 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -221,6 +221,7 @@ pub fn table(name: impl Into) -> TableFactor { alias: None, args: None, with_hints: vec![], + version: None, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index bbe1a6e9f..1416ea790 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -95,6 +95,7 @@ fn parse_table_identifiers() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] },] @@ -143,6 +144,29 @@ fn parse_table_identifiers() { test_table_ident("abc5.GROUP", vec![Ident::new("abc5"), Ident::new("GROUP")]); } +#[test] +fn parse_table_time_travel() { + let version = "2023-08-18 23:08:18".to_string(); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let select = bigquery().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::Timestamp(version)), + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); + assert!(bigquery().parse_sql_statements(&sql).is_err()); +} + #[test] fn parse_join_constraint_unnest_alias() { assert_eq!( diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 24c641561..77b936d55 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -62,6 +62,7 @@ fn parse_map_access_expr() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -169,11 +170,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8dfcc6e7f..96dac3da0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -214,6 +214,7 @@ fn parse_update_set_from() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -240,6 +241,7 @@ fn parse_update_set_from() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -303,6 +305,7 @@ fn parse_update_with_table_alias() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -365,6 +368,7 @@ fn parse_select_with_table_alias() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }] @@ -395,6 +399,7 @@ fn parse_delete_statement() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -422,6 +427,7 @@ fn parse_delete_statement_for_multi_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -431,6 +437,7 @@ fn parse_delete_statement_for_multi_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].joins[0].relation ); @@ -454,6 +461,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation ); @@ -463,6 +471,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, from[1].relation ); @@ -472,6 +481,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, using[0].relation ); @@ -481,6 +491,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { alias: None, args: None, with_hints: vec![], + version: None, }, using[0].joins[0].relation ); @@ -508,6 +519,7 @@ fn parse_where_delete_statement() { alias: None, args: None, with_hints: vec![], + version: None, }, from[0].relation, ); @@ -549,6 +561,7 @@ fn parse_where_delete_with_alias_statement() { }), args: None, with_hints: vec![], + version: None, }, from[0].relation, ); @@ -562,6 +575,7 @@ fn parse_where_delete_with_alias_statement() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![], }]), @@ -3564,6 +3578,7 @@ fn test_parse_named_window() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -3902,6 +3917,7 @@ fn parse_interval_and_or_xor() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -4506,6 +4522,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -4515,6 +4532,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }, @@ -4532,6 +4550,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -4539,6 +4558,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4549,6 +4569,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -4556,6 +4577,7 @@ fn parse_implicit_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -4576,6 +4598,7 @@ fn parse_cross_join() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::CrossJoin, }, @@ -4596,6 +4619,7 @@ fn parse_joins_on() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -4665,6 +4689,7 @@ fn parse_joins_using() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } @@ -4726,6 +4751,7 @@ fn parse_natural_join() { alias, args: None, with_hints: vec![], + version: None, }, join_operator: f(JoinConstraint::Natural), } @@ -4990,6 +5016,7 @@ fn parse_derived_tables() { alias: None, args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6317,6 +6344,7 @@ fn parse_merge() { }), args: None, with_hints: vec![], + version: None, } ); assert_eq!(table, table_no_into); @@ -6340,6 +6368,7 @@ fn parse_merge() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 83b1e537c..3587e8d90 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -155,6 +155,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -187,6 +188,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -228,6 +230,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], @@ -260,6 +263,7 @@ fn test_select_union_by_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index ddc5a8ccf..8cdfe9248 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -322,11 +322,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 56fbd576e..325b116af 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -42,6 +42,29 @@ fn parse_mssql_identifiers() { }; } +#[test] +fn parse_table_time_travel() { + let version = "2023-08-18 23:08:18".to_string(); + let sql = format!("SELECT 1 FROM t1 FOR SYSTEM_TIME AS OF '{version}'"); + let select = ms().verified_only_select(&sql); + assert_eq!( + select.from, + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident::new("t1")]), + alias: None, + args: None, + with_hints: vec![], + version: Some(TableVersion::Timestamp(version)), + }, + joins: vec![] + },] + ); + + let sql = "SELECT 1 FROM t1 FOR SYSTEM TIME AS OF 'some_timestamp'".to_string(); + assert!(ms().parse_sql_statements(&sql).is_err()); +} + #[test] fn parse_mssql_single_quoted_aliases() { let _ = ms_and_generic().one_statement_parses_to("SELECT foo 'alias'", "SELECT foo AS 'alias'"); @@ -283,11 +306,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } @@ -465,7 +490,8 @@ fn parse_substring_in_select() { }]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], + version: None, }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index c0a51edab..3e042df5b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1068,6 +1068,7 @@ fn parse_select_with_numeric_prefix_column_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -1116,6 +1117,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![] }], @@ -1175,6 +1177,7 @@ fn parse_update_with_joins() { }), args: None, with_hints: vec![], + version: None, }, joins: vec![Join { relation: TableFactor::Table { @@ -1185,6 +1188,7 @@ fn parse_update_with_joins() { }), args: None, with_hints: vec![], + version: None, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -1299,7 +1303,8 @@ fn parse_substring_in_select() { }]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], + version: None, }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a62c41e42..b3621a34b 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -2855,11 +2855,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index c44f6dee4..9f5f62f78 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -45,6 +45,7 @@ fn test_square_brackets_over_db_schema_table_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], } @@ -89,6 +90,7 @@ fn test_double_quotes_over_db_schema_table_name() { alias: None, args: None, with_hints: vec![], + version: None, }, joins: vec![], } @@ -108,11 +110,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 43ebb8b11..200849896 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -223,11 +223,13 @@ fn parse_delimited_identifiers() { alias, args, with_hints, + version, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); assert_eq!(Ident::with_quote('"', "alias"), alias.unwrap().name); assert!(args.is_none()); assert!(with_hints.is_empty()); + assert!(version.is_none()); } _ => panic!("Expecting TableFactor::Table"), } From 7e5df3046e316cc3916083d76d33887afc084ce9 Mon Sep 17 00:00:00 2001 From: Marko Grujic Date: Tue, 22 Aug 2023 09:23:27 +0200 Subject: [PATCH 2/2] Enable arbitrary expression support in the temporal qualifier Also rename the current enum variant to ForSystemTimeAsOf to make it more explicit. --- src/ast/query.rs | 6 ++---- src/ast/visitor.rs | 2 -- src/parser/mod.rs | 12 ++---------- tests/sqlparser_bigquery.rs | 4 +++- tests/sqlparser_mssql.rs | 4 +++- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 859e0398b..b70017654 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -847,15 +847,13 @@ impl fmt::Display for TableAlias { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum TableVersion { - Timestamp(String), + ForSystemTimeAsOf(Expr), } impl Display for TableVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - TableVersion::Timestamp(timestamp) => { - write!(f, " FOR SYSTEM_TIME AS OF '{timestamp}'")? - } + TableVersion::ForSystemTimeAsOf(e) => write!(f, " FOR SYSTEM_TIME AS OF {e}")?, } Ok(()) } diff --git a/src/ast/visitor.rs b/src/ast/visitor.rs index c30222dc2..bb7c19678 100644 --- a/src/ast/visitor.rs +++ b/src/ast/visitor.rs @@ -123,7 +123,6 @@ visit_noop!(bigdecimal::BigDecimal); /// /// These methods provide a more concise way of visiting nodes of a certain type: /// * [visit_relations] -/// * [visit_table_factors] /// * [visit_expressions] /// * [visit_statements] /// @@ -231,7 +230,6 @@ pub trait Visitor { /// /// These methods provide a more concise way of visiting nodes of a certain type: /// * [visit_relations_mut] -/// * [visit_table_factors_mut] /// * [visit_expressions_mut] /// * [visit_statements_mut] /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 98131cedd..2251d93ff 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6243,16 +6243,8 @@ impl<'a> Parser<'a> { if dialect_of!(self is BigQueryDialect | MsSqlDialect) && self.parse_keywords(&[Keyword::FOR, Keyword::SYSTEM_TIME, Keyword::AS, Keyword::OF]) { - let timestamp_token = self.next_token(); - match timestamp_token.token { - Token::SingleQuotedString(timestamp) => { - Ok(Some(TableVersion::Timestamp(timestamp))) - } - _ => self.expected( - "Expected Token::SingleQuotedString after FOR SYSTEM_TIME AS OF", - timestamp_token, - ), - } + let expr = self.parse_expr()?; + Ok(Some(TableVersion::ForSystemTimeAsOf(expr))) } else { Ok(None) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 1416ea790..ca711b26e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -157,7 +157,9 @@ fn parse_table_time_travel() { alias: None, args: None, with_hints: vec![], - version: Some(TableVersion::Timestamp(version)), + version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( + Value::SingleQuotedString(version) + ))), }, joins: vec![] },] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 325b116af..23a7bebc7 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -55,7 +55,9 @@ fn parse_table_time_travel() { alias: None, args: None, with_hints: vec![], - version: Some(TableVersion::Timestamp(version)), + version: Some(TableVersion::ForSystemTimeAsOf(Expr::Value( + Value::SingleQuotedString(version) + ))), }, joins: vec![] },]