diff --git a/src/ast/query.rs b/src/ast/query.rs index 7d2626b2d..d44e23667 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -905,6 +905,10 @@ pub enum TableFactor { /// Optional version qualifier to facilitate table time-travel, as /// supported by BigQuery and MSSQL. version: Option, + // Optional table function modifier to generate the ordinality for column. + /// For example, `SELECT * FROM generate_series(1, 10) WITH ORDINALITY AS t(a, b);` + /// [WITH ORDINALITY](https://www.postgresql.org/docs/current/functions-srf.html), supported by Postgres. + with_ordinality: bool, /// [Partition selection](https://dev.mysql.com/doc/refman/8.0/en/partitioning-selection.html), supported by MySQL. partitions: Vec, }, @@ -940,6 +944,7 @@ pub enum TableFactor { array_exprs: Vec, with_offset: bool, with_offset_alias: Option, + with_ordinality: bool, }, /// The `JSON_TABLE` table-valued function. /// Part of the SQL standard, but implemented only by MySQL, Oracle, and DB2. @@ -1285,6 +1290,7 @@ impl fmt::Display for TableFactor { with_hints, version, partitions, + with_ordinality, } => { write!(f, "{name}")?; if !partitions.is_empty() { @@ -1293,6 +1299,9 @@ impl fmt::Display for TableFactor { if let Some(args) = args { write!(f, "({})", display_comma_separated(args))?; } + if *with_ordinality { + write!(f, " WITH ORDINALITY")?; + } if let Some(alias) = alias { write!(f, " AS {alias}")?; } @@ -1346,9 +1355,14 @@ impl fmt::Display for TableFactor { array_exprs, with_offset, with_offset_alias, + with_ordinality, } => { write!(f, "UNNEST({})", display_comma_separated(array_exprs))?; + if *with_ordinality { + write!(f, " WITH ORDINALITY")?; + } + if let Some(alias) = alias { write!(f, " AS {alias}")?; } diff --git a/src/keywords.rs b/src/keywords.rs index eb69a209b..54c1778e4 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -518,6 +518,7 @@ define_keywords!( OR, ORC, ORDER, + ORDINALITY, OUT, OUTER, OUTPUTFORMAT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1dc6bff5e..c32d9177c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -9152,6 +9152,7 @@ impl<'a> Parser<'a> { 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.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS) { Ok(Some(alias)) => Some(alias), Ok(None) => None, @@ -9178,6 +9179,7 @@ impl<'a> Parser<'a> { array_exprs, with_offset, with_offset_alias, + with_ordinality, }) } else if self.parse_keyword_with_tokens(Keyword::JSON_TABLE, &[Token::LParen]) { let json_expr = self.parse_expr()?; @@ -9216,6 +9218,8 @@ impl<'a> Parser<'a> { None }; + let with_ordinality = self.parse_keywords(&[Keyword::WITH, Keyword::ORDINALITY]); + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; // MSSQL-specific table hints: @@ -9237,6 +9241,7 @@ impl<'a> Parser<'a> { with_hints, version, partitions, + with_ordinality, }; while let Some(kw) = self.parse_one_of_keywords(&[Keyword::PIVOT, Keyword::UNPIVOT]) { diff --git a/src/test_utils.rs b/src/test_utils.rs index 1a31d4611..1f5300be1 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -309,6 +309,7 @@ pub fn table(name: impl Into) -> TableFactor { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } } @@ -323,6 +324,7 @@ pub fn table_with_alias(name: impl Into, alias: impl Into) -> Ta with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 88e2ef912..089a41889 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -224,6 +224,7 @@ fn parse_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -1353,6 +1354,7 @@ fn parse_table_identifiers() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -1525,6 +1527,7 @@ fn parse_table_time_travel() { Value::SingleQuotedString(version) ))), partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -1551,7 +1554,8 @@ fn parse_join_constraint_unnest_alias() { Ident::new("a") ])], with_offset: false, - with_offset_alias: None + with_offset_alias: None, + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -1620,6 +1624,7 @@ fn parse_merge() { with_hints: Default::default(), version: Default::default(), partitions: Default::default(), + with_ordinality: false, }, table ); @@ -1634,6 +1639,7 @@ fn parse_merge() { with_hints: Default::default(), version: Default::default(), partitions: Default::default(), + with_ordinality: false, }, source ); diff --git a/tests/sqlparser_clickhouse.rs b/tests/sqlparser_clickhouse.rs index 29a5b15aa..a92942a6e 100644 --- a/tests/sqlparser_clickhouse.rs +++ b/tests/sqlparser_clickhouse.rs @@ -59,6 +59,7 @@ fn parse_map_access_expr() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -162,6 +163,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 2b208016a..19669f732 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -359,6 +359,7 @@ fn parse_update_set_from() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -387,6 +388,7 @@ fn parse_update_set_from() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -462,6 +464,7 @@ fn parse_update_with_table_alias() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -529,6 +532,7 @@ fn parse_select_with_table_alias() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }] @@ -565,6 +569,7 @@ fn parse_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -611,6 +616,7 @@ fn parse_delete_statement_for_multi_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -622,6 +628,7 @@ fn parse_delete_statement_for_multi_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].joins[0].relation ); @@ -647,6 +654,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation ); @@ -658,6 +666,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[1].relation ); @@ -669,6 +678,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, using[0].relation ); @@ -680,6 +690,7 @@ fn parse_delete_statement_for_multi_tables_with_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, using[0].joins[0].relation ); @@ -710,6 +721,7 @@ fn parse_where_delete_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation, ); @@ -754,6 +766,7 @@ fn parse_where_delete_with_alias_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, from[0].relation, ); @@ -769,6 +782,7 @@ fn parse_where_delete_with_alias_statement() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }]), @@ -4548,6 +4562,7 @@ fn test_parse_named_window() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -4930,6 +4945,7 @@ fn parse_interval_and_or_xor() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -5282,6 +5298,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5299,6 +5316,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5316,6 +5334,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: true, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5336,6 +5355,7 @@ fn parse_unnest_in_from_clause() { array_exprs: vec![Expr::Identifier(Ident::new("expr"))], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5360,6 +5380,7 @@ fn parse_unnest_in_from_clause() { )], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5390,6 +5411,7 @@ fn parse_unnest_in_from_clause() { ], with_offset: false, with_offset_alias: None, + with_ordinality: false, }, joins: vec![], }], @@ -5499,6 +5521,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -5510,6 +5533,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }, @@ -5529,6 +5553,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -5538,6 +5563,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -5550,6 +5576,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -5559,6 +5586,7 @@ fn parse_implicit_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -5581,6 +5609,7 @@ fn parse_cross_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::CrossJoin, }, @@ -5603,6 +5632,7 @@ fn parse_joins_on() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -5674,6 +5704,7 @@ fn parse_joins_using() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::Using(vec!["c1".into()])), } @@ -5737,6 +5768,7 @@ fn parse_natural_join() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: f(JoinConstraint::Natural), } @@ -6004,6 +6036,7 @@ fn parse_derived_tables() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], @@ -6901,6 +6934,7 @@ fn lateral_function() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Function { @@ -7609,6 +7643,7 @@ fn parse_merge() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, } ); assert_eq!(table, table_no_into); @@ -7634,6 +7669,7 @@ fn parse_merge() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -8695,6 +8731,7 @@ fn parse_pivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), aggregate_functions: vec![ expected_function("a", None), @@ -8764,6 +8801,7 @@ fn parse_unpivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), value: Ident { value: "quantity".to_string(), @@ -8830,6 +8868,7 @@ fn parse_pivot_unpivot_table() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }), value: Ident { value: "population".to_string(), @@ -9154,6 +9193,7 @@ fn parse_unload() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9298,6 +9338,7 @@ fn parse_connect_by() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9383,6 +9424,7 @@ fn parse_connect_by() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -9542,6 +9584,7 @@ fn test_match_recognize() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }; fn check(options: &str, expect: TableFactor) { diff --git a/tests/sqlparser_databricks.rs b/tests/sqlparser_databricks.rs index 90056f0f7..280b97b49 100644 --- a/tests/sqlparser_databricks.rs +++ b/tests/sqlparser_databricks.rs @@ -177,7 +177,8 @@ fn test_values_clause() { args: None, with_hints: vec![], version: None, - partitions: vec![] + partitions: vec![], + with_ordinality: false, }), query .body diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index 400daa8a8..0e61b86c9 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -166,6 +166,7 @@ fn test_select_union_by_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], @@ -205,6 +206,7 @@ fn test_select_union_by_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], }], diff --git a/tests/sqlparser_hive.rs b/tests/sqlparser_hive.rs index 53280d7d8..5f0b9f575 100644 --- a/tests/sqlparser_hive.rs +++ b/tests/sqlparser_hive.rs @@ -359,6 +359,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index e0e0f7c70..c80663c77 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -64,6 +64,7 @@ fn parse_table_time_travel() { Value::SingleQuotedString(version) ))), partitions: vec![], + with_ordinality: false, }, joins: vec![] },] @@ -334,6 +335,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); @@ -525,6 +527,7 @@ fn parse_substring_in_select() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index a5fa75200..b3d8b202f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1715,6 +1715,7 @@ fn parse_select_with_numeric_prefix_column_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], @@ -1769,6 +1770,7 @@ fn parse_select_with_concatenation_of_exp_number_and_numeric_prefix_column() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], @@ -1834,6 +1836,7 @@ fn parse_update_with_joins() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![Join { relation: TableFactor::Table { @@ -1846,6 +1849,7 @@ fn parse_update_with_joins() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ @@ -2269,6 +2273,7 @@ fn parse_substring_in_select() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![] }], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 2d3097cf9..962c6cced 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -3427,6 +3427,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); @@ -3878,7 +3879,8 @@ fn parse_join_constraint_unnest_alias() { Ident::new("a") ])], with_offset: false, - with_offset_alias: None + with_offset_alias: None, + with_ordinality: false, }, join_operator: JoinOperator::Inner(JoinConstraint::On(Expr::BinaryOp { left: Box::new(Expr::Identifier("c1".into())), @@ -4183,3 +4185,36 @@ fn parse_create_table_with_options() { _ => unreachable!(), } } + +#[test] +fn test_table_function_with_ordinality() { + let from = pg_and_generic() + .verified_only_select("SELECT * FROM generate_series(1, 10) WITH ORDINALITY AS t") + .from; + assert_eq!(1, from.len()); + match from[0].relation { + TableFactor::Table { + ref name, + with_ordinality: true, + .. + } => { + assert_eq!("generate_series", name.to_string().as_str()); + } + _ => panic!("Expecting TableFactor::Table with ordinality"), + } +} + +#[test] +fn test_table_unnest_with_ordinality() { + let from = pg_and_generic() + .verified_only_select("SELECT * FROM UNNEST([10, 20, 30]) WITH ORDINALITY AS t") + .from; + assert_eq!(1, from.len()); + match from[0].relation { + TableFactor::UNNEST { + with_ordinality: true, + .. + } => {} + _ => panic!("Expecting TableFactor::UNNEST with ordinality"), + } +} diff --git a/tests/sqlparser_redshift.rs b/tests/sqlparser_redshift.rs index 938e6e887..440116e02 100644 --- a/tests/sqlparser_redshift.rs +++ b/tests/sqlparser_redshift.rs @@ -48,6 +48,7 @@ fn test_square_brackets_over_db_schema_table_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], } @@ -94,6 +95,7 @@ fn test_double_quotes_over_db_schema_table_name() { with_hints: vec![], version: None, partitions: vec![], + with_ordinality: false, }, joins: vec![], } @@ -114,6 +116,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2f4ed1316..7a2288cbb 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -870,6 +870,7 @@ fn parse_delimited_identifiers() { args, with_hints, version, + with_ordinality: _, partitions: _, } => { assert_eq!(vec![Ident::with_quote('"', "a table")], name.0); diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index dd1e77d5d..629ab5fc2 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -399,7 +399,8 @@ fn parse_update_tuple_row_values() { args: None, with_hints: vec![], version: None, - partitions: vec![] + partitions: vec![], + with_ordinality: false, }, joins: vec![], },