diff --git a/src/ast/query.rs b/src/ast/query.rs index 296e4e8ca..1b56649c2 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1513,6 +1513,15 @@ pub enum TableFactor { /// Optional alias for the table function result. alias: Option, }, + /// `ROWS FROM(function_call [, ...]) [WITH ORDINALITY][ AS ]` + RowsFrom { + /// Table functions combined by `ROWS FROM`. + table_functions: Vec, + /// Whether `WITH ORDINALITY` was specified to include ordinality. + with_ordinality: bool, + /// Optional alias for the `ROWS FROM` result. + alias: Option, + }, /// `e.g. LATERAL FLATTEN()[ AS ]` Function { /// Whether the function is LATERAL. @@ -2316,6 +2325,20 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::RowsFrom { + table_functions, + with_ordinality, + alias, + } => { + write!(f, "ROWS FROM({})", display_comma_separated(table_functions))?; + if *with_ordinality { + write!(f, " WITH ORDINALITY")?; + } + if let Some(alias) = alias { + write!(f, " {alias}")?; + } + Ok(()) + } TableFactor::UNNEST { alias, array_exprs, diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 7acbd7d0b..20f4dcc08 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -2001,6 +2001,16 @@ impl Spanned for TableFactor { TableFactor::TableFunction { expr, alias } => expr .span() .union_opt(&alias.as_ref().map(|alias| alias.span())), + TableFactor::RowsFrom { + table_functions, + with_ordinality: _, + alias, + } => union_spans( + table_functions + .iter() + .map(|i| i.span()) + .chain(alias.as_ref().map(|alias| alias.span())), + ), TableFactor::UNNEST { alias, with_offset: _, diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index d408cb181..b8a345cc5 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -77,6 +77,10 @@ impl Dialect for GenericDialect { true } + fn supports_rows_from_table_factor(&self) -> bool { + true + } + fn supports_start_transaction_modifier(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 7c4744c5a..300409703 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -732,6 +732,13 @@ pub trait Dialect: Debug + Any { false } + /// Return true if the dialect supports `ROWS FROM` in table functions. + /// + /// See + fn supports_rows_from_table_factor(&self) -> bool { + false + } + /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax? fn supports_user_host_grantee(&self) -> bool { false diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 3bec6ceba..b48145783 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -255,6 +255,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_rows_from_table_factor(&self) -> bool { + true + } + /// See /// /// Required to support the colon in: diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 15f135fff..42ba3f561 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16611,6 +16611,7 @@ impl<'a> Parser<'a> { | TableFactor::XmlTable { alias, .. } | TableFactor::OpenJsonTable { alias, .. } | TableFactor::TableFunction { alias, .. } + | TableFactor::RowsFrom { alias, .. } | TableFactor::Pivot { alias, .. } | TableFactor::Unpivot { alias, .. } | TableFactor::MatchRecognize { alias, .. } @@ -16678,42 +16679,23 @@ impl<'a> Parser<'a> { alias, sample: None, }) - } else if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) - && self.parse_keyword(Keyword::UNNEST) + } else if self.dialect.supports_rows_from_table_factor() + && self.parse_keywords(&[Keyword::ROWS, Keyword::FROM]) { self.expect_token(&Token::LParen)?; - let array_exprs = self.parse_comma_separated(Parser::parse_expr)?; + let table_functions = self.parse_comma_separated(Parser::parse_rows_from_function)?; self.expect_token(&Token::RParen)?; - let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); - let alias = match self.maybe_parse_table_alias() { - Ok(Some(alias)) => Some(alias), - Ok(None) => None, - Err(e) => return Err(e), - }; - - let with_offset = match self.expect_keywords(&[Keyword::WITH, Keyword::OFFSET]) { - Ok(()) => true, - Err(_) => false, - }; - - let with_offset_alias = if with_offset { - match self.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) { - Ok(Some(alias)) => Some(alias), - Ok(None) => None, - Err(e) => return Err(e), - } - } else { - None - }; - - Ok(TableFactor::UNNEST { - alias, - array_exprs, - with_offset, - with_offset_alias, + let alias = self.maybe_parse_table_alias()?; + Ok(TableFactor::RowsFrom { + table_functions, with_ordinality, + alias, }) + } else if dialect_of!(self is BigQueryDialect | PostgreSqlDialect | GenericDialect) + && self.parse_keyword(Keyword::UNNEST) + { + self.parse_unnest_table_factor() } else if self.dialect.supports_unpivot_expr() && self.peek_keyword(Keyword::UNPIVOT) { self.parse_unpivot_expr_table_factor() } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { @@ -16841,6 +16823,67 @@ impl<'a> Parser<'a> { } } + fn parse_rows_from_function(&mut self) -> Result { + if self.parse_keyword(Keyword::UNNEST) { + return self.parse_unnest_table_factor(); + } + + let name = self.parse_object_name(true)?; + self.expect_token(&Token::LParen)?; + let args = Some(self.parse_table_function_args()?); + let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); + let alias = self.maybe_parse_table_alias()?; + + Ok(TableFactor::Table { + name, + alias, + args, + with_hints: vec![], + version: None, + partitions: vec![], + with_ordinality, + json_path: None, + sample: None, + index_hints: vec![], + }) + } + + fn parse_unnest_table_factor(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let array_exprs = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + + let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); + let alias = match self.maybe_parse_table_alias() { + Ok(Some(alias)) => Some(alias), + Ok(None) => None, + Err(e) => return Err(e), + }; + + let with_offset = match self.expect_keywords(&[Keyword::WITH, Keyword::OFFSET]) { + Ok(()) => true, + Err(_) => false, + }; + + let with_offset_alias = if with_offset { + match self.parse_optional_alias(keywords::RESERVED_FOR_COLUMN_ALIAS) { + Ok(Some(alias)) => Some(alias), + Ok(None) => None, + Err(e) => return Err(e), + } + } else { + None + }; + + Ok(TableFactor::UNNEST { + alias, + array_exprs, + with_offset, + with_offset_alias, + with_ordinality, + }) + } + /// Parse a Snowflake stage reference as a table factor. /// Handles syntax like: `@mystage1 (file_format => 'myformat', pattern => '...')` /// diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index d71e49b27..e6ec1009c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9953,3 +9953,23 @@ fn parse_insert_by_name_keywords_as_table_and_alias() { statement => panic!("Expected INSERT statement, got: {statement:?}"), } } + +#[test] +fn parse_rows_from_with_ordinality() { + pg_and_generic().verified_stmt( + "SELECT * FROM ROWS FROM(UNNEST(ARRAY[1, 2]) WITH ORDINALITY) AS r (val, ord)", + ); + + pg_and_generic().verified_stmt( + "SELECT * FROM ROWS FROM(UNNEST(ARRAY[1, 2])) WITH ORDINALITY AS r (val, ord)", + ); + + pg_and_generic().verified_stmt( + "SELECT * FROM ROWS FROM(UNNEST(ARRAY[1]), generate_series(1, 2)) AS r (a, b)", + ); + + let err = pg_and_generic() + .parse_sql_statements("SELECT * FROM ROWS FROM(foo)") + .unwrap_err(); + assert_eq!("sql parser error: Expected: (, found: )", err.to_string()); +}