From 36fea1ca18b8fddec45cd2f2fa5bb5f471bf91d5 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Tue, 5 Nov 2024 00:52:06 +0800 Subject: [PATCH 01/15] Add support for MSSQL's `OPENJSON WITH` clause --- src/ast/mod.rs | 11 +++--- src/ast/query.rs | 74 ++++++++++++++++++++++++++++++++++++++++ src/keywords.rs | 1 + src/parser/mod.rs | 55 ++++++++++++++++++++++++++++- tests/sqlparser_mssql.rs | 29 ++++++++++++++++ 5 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b2672552e..f19523fa4 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -56,11 +56,12 @@ pub use self::query::{ InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, - OffsetRows, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, - RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, - SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, - TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, - ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, + OffsetRows, OpenJsonTableColumn, OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, + Query, RenameSelectItem, RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, + RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, + SymbolDefinition, Table, TableAlias, TableFactor, TableFunctionArgs, TableVersion, + TableWithJoins, Top, TopQuantity, ValueTableMode, Values, WildcardAdditionalOptions, With, + WithFill, }; pub use self::trigger::{ diff --git a/src/ast/query.rs b/src/ast/query.rs index dc5966e5e..08bc37136 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -1026,6 +1026,27 @@ pub enum TableFactor { /// The alias for the table. alias: Option, }, + /// The MSSQL's `OPENJSON` table-valued function. + /// + /// ```sql + /// OPENJSON( jsonExpression [ , path ] ) [ ] + /// + /// ::= WITH ( { colName type [ column_path ] [ AS JSON ] } [ ,...n ] ) + /// ```` + /// + /// Reference: + OpenJsonTable { + /// The JSON expression to be evaluated. It must evaluate to a json string + json_expr: Expr, + /// The path to the array or object to be iterated over. + /// It must evaluate to a json array or object. + json_path: Option, + /// The columns to be extracted from each element of the array or object. + /// Each column must have a name and a type. + columns: Vec, + /// The alias for the table. + alias: Option, + }, /// Represents a parenthesized table factor. The SQL spec only allows a /// join expression (`(foo bar [ baz ... ])`) to be nested, /// possibly several times. @@ -1451,6 +1472,25 @@ impl fmt::Display for TableFactor { } Ok(()) } + TableFactor::OpenJsonTable { + json_expr, + json_path, + columns, + alias, + } => { + write!(f, "OPENJSON({json_expr}")?; + if let Some(json_path) = json_path { + write!(f, ", {json_path}")?; + } + write!(f, ")")?; + if !columns.is_empty() { + write!(f, " WITH ({})", display_comma_separated(columns))?; + } + if let Some(alias) = alias { + write!(f, " AS {alias}")?; + } + Ok(()) + } TableFactor::NestedJoin { table_with_joins, alias, @@ -2346,6 +2386,40 @@ impl fmt::Display for JsonTableColumnErrorHandling { } } +/// A single column definition in MSSQL's `OPENJSON WITH` clause. +/// +/// ```sql +/// colName type [ column_path ] [ AS JSON ] +/// ``` +/// +/// Reference: +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OpenJsonTableColumn { + /// The name of the column to be extracted. + pub name: Ident, + /// The type of the column to be extracted. + pub r#type: DataType, + /// The path to the column to be extracted. Must be a literal string. + pub path: Option, + /// The `AS JSON` option. + pub as_json: bool, +} + +impl fmt::Display for OpenJsonTableColumn { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} {}", self.name, self.r#type)?; + if let Some(path) = &self.path { + write!(f, " {}", path)?; + } + if self.as_json { + write!(f, " AS JSON")?; + } + Ok(()) + } +} + /// BigQuery supports ValueTables which have 2 modes: /// `SELECT AS STRUCT` /// `SELECT AS VALUE` diff --git a/src/keywords.rs b/src/keywords.rs index e98309681..fa39e6c10 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -535,6 +535,7 @@ define_keywords!( ONE, ONLY, OPEN, + OPENJSON, OPERATOR, OPTIMIZE, OPTIMIZER_COSTS, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4b92ba4e..ace74d780 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10000,7 +10000,7 @@ impl<'a> Parser<'a> { table_with_joins: Box::new(table_and_joins), alias, }) - } else if dialect_of!(self is SnowflakeDialect | GenericDialect) { + } else if dialect_of!(self is SnowflakeDialect | GenericDialect | MsSqlDialect) { // Dialect-specific behavior: Snowflake diverges from the // standard and from most of the other implementations by // allowing extra parentheses not only around a join (B), but @@ -10020,6 +10020,7 @@ impl<'a> Parser<'a> { | TableFactor::Function { alias, .. } | TableFactor::UNNEST { alias, .. } | TableFactor::JsonTable { alias, .. } + | TableFactor::OpenJsonTable { alias, .. } | TableFactor::TableFunction { alias, .. } | TableFactor::Pivot { alias, .. } | TableFactor::Unpivot { alias, .. } @@ -10133,6 +10134,30 @@ impl<'a> Parser<'a> { columns, alias, }) + } else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) { + let json_expr = self.parse_expr()?; + let json_path = if self.consume_token(&Token::Comma) { + Some(self.parse_value()?) + } else { + None + }; + self.expect_token(&Token::RParen)?; + let columns = if self.parse_keyword(Keyword::WITH) { + self.expect_token(&Token::LParen)?; + let columns = + self.parse_comma_separated(Parser::parse_openjson_table_column_def)?; + self.expect_token(&Token::RParen)?; + columns + } else { + Vec::new() + }; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::OpenJsonTable { + json_expr, + json_path, + columns, + alias, + }) } else { let name = self.parse_object_name(true)?; @@ -10468,6 +10493,34 @@ impl<'a> Parser<'a> { }) } + /// Parses MSSQL's `OPENJSON WITH` column definition. + /// + /// ```sql + /// colName type [ column_path ] [ AS JSON ] + /// ``` + /// + /// Reference: + pub fn parse_openjson_table_column_def(&mut self) -> Result { + let name = self.parse_identifier(false)?; + let r#type = self.parse_data_type()?; + let path = if let Token::SingleQuotedString(path) = self.peek_token().token { + self.next_token(); + Some(Value::SingleQuotedString(path)) + } else { + None + }; + let as_json = self.parse_keyword(Keyword::AS); + if as_json { + self.expect_keyword(Keyword::JSON)?; + } + Ok(OpenJsonTableColumn { + name, + r#type, + path, + as_json, + }) + } + fn parse_json_table_column_error_handling( &mut self, ) -> Result, ParserError> { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0223e2915..9f20e8705 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -192,6 +192,35 @@ fn parse_mssql_apply_join() { ); } +#[test] +fn parse_mssql_cross_apply_json() { + let _ = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param, '$.config') WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", + ); + let _ = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", + ); + let _ = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20), [id_list] NVARCHAR(MAX) AS JSON) AS B", + ); + let _ = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param, '$.config') AS B", + ); + let _ = ms().verified_only_select( + "SELECT B.kind, B.id_list \ + FROM t_test_table AS A \ + CROSS APPLY OPENJSON(A.param) AS B", + ); +} + #[test] fn parse_mssql_top_paren() { let sql = "SELECT TOP (5) * FROM foo"; From d1edf69da2d47f746154b8bb574296ef648ad982 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Wed, 6 Nov 2024 17:16:17 +0800 Subject: [PATCH 02/15] Add support for MSSQL's `XQuery` methods --- src/ast/mod.rs | 10 +++++++++ src/parser/mod.rs | 44 ++++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 10 +++++++++ 3 files changed, 64 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b2672552e..6f0d128dd 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -560,6 +560,13 @@ pub enum Expr { expr: Box, key: Ident, }, + /// CompositeFunction (mssql) e.g. SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// + CompositeFunction { + expr: Box, + name: Ident, + args: Vec, + }, /// `IS FALSE` operator IsFalse(Box), /// `IS NOT FALSE` operator @@ -1629,6 +1636,9 @@ impl fmt::Display for Expr { Expr::CompositeAccess { expr, key } => { write!(f, "{expr}.{key}") } + Expr::CompositeFunction { expr, name, args } => { + write!(f, "{expr}.{name}({})", display_comma_separated(args)) + } Expr::AtTimeZone { timestamp, time_zone, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c4b92ba4e..675c27f0f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1269,6 +1269,50 @@ impl<'a> Parser<'a> { _ => self.expected("an expression", next_token), }?; + if dialect_of!(self is MsSqlDialect) { + // Convert `CompositeAccess` to `CompositeFunction` (MSSQL doesn't support `CompositeAccess` syntax) + // ```sql + // SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + // ``` + if let Expr::CompositeAccess { expr, key } = expr { + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + return Ok(Expr::CompositeFunction { + expr: expr, + name: key, + args, + }); + } + // ```sql + // SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)') + // ``` + else if matches!( + expr, + Expr::Cast { .. } | Expr::Convert { .. } | Expr::Function(_) + ) && self.consume_token(&Token::Period) + { + let tok = self.next_token(); + let name = match tok.token { + Token::Word(word) => word.to_ident(), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.location + ) + } + }; + self.expect_token(&Token::LParen)?; + let args = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + return Ok(Expr::CompositeFunction { + expr: Box::new(expr), + name, + args, + }); + } + } + if self.parse_keyword(Keyword::COLLATE) { Ok(Expr::Collate { expr: Box::new(expr), diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0223e2915..7d70e6126 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1029,6 +1029,16 @@ fn parse_create_table_with_identity_column() { } } +#[test] +fn parse_mssql_xquery() { + let sql = "SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"; + let _ = ms().verified_query(sql); + let sql = "SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"; + let _ = ms().verified_query(sql); + let sql = "SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)') AS T"; + let _ = ms().verified_query(sql); +} + fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } From b07bfc54d160f54cf364d9adcc3a08a08d51d571 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Thu, 7 Nov 2024 11:34:38 +0800 Subject: [PATCH 03/15] Code review comments --- src/ast/query.rs | 4 +- src/parser/mod.rs | 57 ++++--- tests/sqlparser_mssql.rs | 320 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 347 insertions(+), 34 deletions(-) diff --git a/src/ast/query.rs b/src/ast/query.rs index 08bc37136..a2999040d 100644 --- a/src/ast/query.rs +++ b/src/ast/query.rs @@ -2402,7 +2402,7 @@ pub struct OpenJsonTableColumn { /// The type of the column to be extracted. pub r#type: DataType, /// The path to the column to be extracted. Must be a literal string. - pub path: Option, + pub path: Option, /// The `AS JSON` option. pub as_json: bool, } @@ -2411,7 +2411,7 @@ impl fmt::Display for OpenJsonTableColumn { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {}", self.name, self.r#type)?; if let Some(path) = &self.path { - write!(f, " {}", path)?; + write!(f, " '{}'", value::escape_single_quote_string(path))?; } if self.as_json { write!(f, " AS JSON")?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ace74d780..f22376be3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -10000,7 +10000,7 @@ impl<'a> Parser<'a> { table_with_joins: Box::new(table_and_joins), alias, }) - } else if dialect_of!(self is SnowflakeDialect | GenericDialect | MsSqlDialect) { + } else if dialect_of!(self is SnowflakeDialect | GenericDialect) { // Dialect-specific behavior: Snowflake diverges from the // standard and from most of the other implementations by // allowing extra parentheses not only around a join (B), but @@ -10135,29 +10135,8 @@ impl<'a> Parser<'a> { alias, }) } else if self.parse_keyword_with_tokens(Keyword::OPENJSON, &[Token::LParen]) { - let json_expr = self.parse_expr()?; - let json_path = if self.consume_token(&Token::Comma) { - Some(self.parse_value()?) - } else { - None - }; - self.expect_token(&Token::RParen)?; - let columns = if self.parse_keyword(Keyword::WITH) { - self.expect_token(&Token::LParen)?; - let columns = - self.parse_comma_separated(Parser::parse_openjson_table_column_def)?; - self.expect_token(&Token::RParen)?; - columns - } else { - Vec::new() - }; - let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; - Ok(TableFactor::OpenJsonTable { - json_expr, - json_path, - columns, - alias, - }) + self.prev_token(); + self.parse_open_json_table_factor() } else { let name = self.parse_object_name(true)?; @@ -10223,6 +10202,34 @@ impl<'a> Parser<'a> { } } + /// Parses `OPENJSON( jsonExpression [ , path ] ) [ ]` clause, + /// assuming the `OPENJSON` keyword was already consumed. + fn parse_open_json_table_factor(&mut self) -> Result { + self.expect_token(&Token::LParen)?; + let json_expr = self.parse_expr()?; + let json_path = if self.consume_token(&Token::Comma) { + Some(self.parse_value()?) + } else { + None + }; + self.expect_token(&Token::RParen)?; + let columns = if self.parse_keyword(Keyword::WITH) { + self.expect_token(&Token::LParen)?; + let columns = self.parse_comma_separated(Parser::parse_openjson_table_column_def)?; + self.expect_token(&Token::RParen)?; + columns + } else { + Vec::new() + }; + let alias = self.parse_optional_table_alias(keywords::RESERVED_FOR_TABLE_ALIAS)?; + Ok(TableFactor::OpenJsonTable { + json_expr, + json_path, + columns, + alias, + }) + } + fn parse_match_recognize(&mut self, table: TableFactor) -> Result { self.expect_token(&Token::LParen)?; @@ -10505,7 +10512,7 @@ impl<'a> Parser<'a> { let r#type = self.parse_data_type()?; let path = if let Token::SingleQuotedString(path) = self.peek_token().token { self.next_token(); - Some(Value::SingleQuotedString(path)) + Some(path) } else { None }; diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 9f20e8705..9da91f275 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -193,32 +193,338 @@ fn parse_mssql_apply_join() { } #[test] -fn parse_mssql_cross_apply_json() { - let _ = ms().verified_only_select( +fn parse_mssql_openjson() { + let select = ms().verified_only_select( "SELECT B.kind, B.id_list \ FROM t_test_table AS A \ CROSS APPLY OPENJSON(A.param, '$.config') WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", ); - let _ = ms().verified_only_select( + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: Some(Value::SingleQuotedString("$.config".into())), + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: Some("$.kind".into()), + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: Some("$.id_list".into()), + as_json: true + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms().verified_only_select( "SELECT B.kind, B.id_list \ FROM t_test_table AS A \ CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20) '$.kind', [id_list] NVARCHAR(MAX) '$.id_list' AS JSON) AS B", ); - let _ = ms().verified_only_select( + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: Some("$.kind".into()), + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: Some("$.id_list".into()), + as_json: true + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms().verified_only_select( "SELECT B.kind, B.id_list \ FROM t_test_table AS A \ - CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20), [id_list] NVARCHAR(MAX) AS JSON) AS B", + CROSS APPLY OPENJSON(A.param) WITH (kind VARCHAR(20), [id_list] NVARCHAR(MAX)) AS B", ); - let _ = ms().verified_only_select( + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![ + OpenJsonTableColumn { + name: Ident { + value: "kind".into(), + quote_style: None, + }, + r#type: DataType::Varchar(Some(CharacterLength::IntegerLength { + length: 20, + unit: None + })), + path: None, + as_json: false + }, + OpenJsonTableColumn { + name: Ident { + value: "id_list".into(), + quote_style: Some('['), + }, + r#type: DataType::Nvarchar(Some(CharacterLength::Max)), + path: None, + as_json: false + } + ], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms_and_generic().verified_only_select( "SELECT B.kind, B.id_list \ FROM t_test_table AS A \ CROSS APPLY OPENJSON(A.param, '$.config') AS B", ); - let _ = ms().verified_only_select( + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: Some(Value::SingleQuotedString("$.config".into())), + columns: vec![], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); + let select = ms_and_generic().verified_only_select( "SELECT B.kind, B.id_list \ FROM t_test_table AS A \ CROSS APPLY OPENJSON(A.param) AS B", ); + assert_eq!( + vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![Ident { + value: "t_test_table".into(), + quote_style: None, + },]), + alias: Some(TableAlias { + name: Ident { + value: "A".into(), + quote_style: None + }, + columns: vec![] + }), + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![Join { + relation: TableFactor::OpenJsonTable { + json_expr: Expr::CompoundIdentifier(vec![ + Ident { + value: "A".into(), + quote_style: None, + }, + Ident { + value: "param".into(), + quote_style: None, + } + ]), + json_path: None, + columns: vec![], + alias: Some(TableAlias { + name: Ident { + value: "B".into(), + quote_style: None + }, + columns: vec![] + }) + }, + global: false, + join_operator: JoinOperator::CrossApply + }] + }], + select.from + ); } #[test] From 342595787ee3e7db3a99ff3ab3dbd6734c0a8989 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Thu, 7 Nov 2024 11:54:37 +0800 Subject: [PATCH 04/15] Resolve conflict --- src/ast/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a24739a60..00fd05558 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -56,12 +56,12 @@ pub use self::query::{ InterpolateExpr, Join, JoinConstraint, JoinOperator, JsonTableColumn, JsonTableColumnErrorHandling, JsonTableNamedColumn, JsonTableNestedColumn, LateralView, LockClause, LockType, MatchRecognizePattern, MatchRecognizeSymbol, Measure, - NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OrderBy, OrderByExpr, - PivotValueSource, ProjectionSelect, Query, RenameSelectItem, RepetitionQuantifier, - ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, SelectInto, SelectItem, SetExpr, - SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, TableAlias, TableFactor, - TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, ValueTableMode, Values, - WildcardAdditionalOptions, With, WithFill, + NamedWindowDefinition, NamedWindowExpr, NonBlock, Offset, OffsetRows, OpenJsonTableColumn, + OrderBy, OrderByExpr, PivotValueSource, ProjectionSelect, Query, RenameSelectItem, + RepetitionQuantifier, ReplaceSelectElement, ReplaceSelectItem, RowsPerMatch, Select, + SelectInto, SelectItem, SetExpr, SetOperator, SetQuantifier, Setting, SymbolDefinition, Table, + TableAlias, TableFactor, TableFunctionArgs, TableVersion, TableWithJoins, Top, TopQuantity, + ValueTableMode, Values, WildcardAdditionalOptions, With, WithFill, }; pub use self::trigger::{ From debf426670ed13ec2a88b8d39fbb802a1b6a186c Mon Sep 17 00:00:00 2001 From: gaoqiangz <38213294+gaoqiangz@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:51:40 +0800 Subject: [PATCH 05/15] Update src/ast/mod.rs Co-authored-by: Ifeanyi Ubah --- src/ast/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6f0d128dd..d64df1084 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -560,8 +560,12 @@ pub enum Expr { expr: Box, key: Ident, }, - /// CompositeFunction (mssql) e.g. SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// + /// CompositeFunction [mssql] + /// Example: + /// ```sql + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// ``` + /// [mssql]: https://learn.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver16 CompositeFunction { expr: Box, name: Ident, From da7953635d38dcfbe0e59708be17d328617e630b Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Thu, 7 Nov 2024 17:06:28 +0800 Subject: [PATCH 06/15] Redesign `CompositeFunction` --- src/ast/mod.rs | 45 ++++-- src/parser/mod.rs | 85 ++++++------ tests/sqlparser_common.rs | 278 ++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 10 -- 4 files changed, 350 insertions(+), 68 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d64df1084..735379ed0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -560,17 +560,6 @@ pub enum Expr { expr: Box, key: Ident, }, - /// CompositeFunction [mssql] - /// Example: - /// ```sql - /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// ``` - /// [mssql]: https://learn.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver16 - CompositeFunction { - expr: Box, - name: Ident, - args: Vec, - }, /// `IS FALSE` operator IsFalse(Box), /// `IS NOT FALSE` operator @@ -818,6 +807,21 @@ pub enum Expr { }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), + /// CompositeFunction (function chain) + /// + /// Syntax: + /// + /// `.....` + /// + /// > `arbitrary-expr` can be any expression including a function call. + /// + /// Example: + /// + /// ```sql + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)') + /// ``` + CompositeFunction(CompositeFunction), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -1474,6 +1478,7 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{fun}"), + Expr::CompositeFunction(fun) => write!(f, "{fun}"), Expr::Case { operand, conditions, @@ -1640,9 +1645,6 @@ impl fmt::Display for Expr { Expr::CompositeAccess { expr, key } => { write!(f, "{expr}.{key}") } - Expr::CompositeFunction { expr, name, args } => { - write!(f, "{expr}.{name}({})", display_comma_separated(args)) - } Expr::AtTimeZone { timestamp, time_zone, @@ -5578,6 +5580,21 @@ impl fmt::Display for FunctionArgumentClause { } } +/// A composite function call +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct CompositeFunction { + pub left: Box, + pub right: Function, +} + +impl fmt::Display for CompositeFunction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}", self.left, self.right,) + } +} + #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 675c27f0f..103a589e7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -993,7 +993,7 @@ impl<'a> Parser<'a> { } let next_token = self.next_token(); - let expr = match next_token.token { + let mut expr = match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE | Keyword::FALSE | Keyword::NULL => { self.prev_token(); @@ -1252,10 +1252,22 @@ impl<'a> Parser<'a> { ) } }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) + if self.consume_token(&Token::LParen) { + self.prev_token(); + let func = match self.parse_function(ObjectName(vec![key]))? { + Expr::Function(func) => func, + _ => unreachable!(), + }; + Ok(Expr::CompositeFunction(CompositeFunction { + left: Box::new(expr), + right: func, + })) + } else { + Ok(Expr::CompositeAccess { + expr: Box::new(expr), + key, + }) + } } } Token::Placeholder(_) | Token::Colon | Token::AtSign => { @@ -1269,46 +1281,31 @@ impl<'a> Parser<'a> { _ => self.expected("an expression", next_token), }?; - if dialect_of!(self is MsSqlDialect) { - // Convert `CompositeAccess` to `CompositeFunction` (MSSQL doesn't support `CompositeAccess` syntax) - // ```sql - // SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - // ``` - if let Expr::CompositeAccess { expr, key } = expr { - self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - return Ok(Expr::CompositeFunction { - expr: expr, - name: key, - args, - }); - } - // ```sql - // SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)') - // ``` - else if matches!( - expr, - Expr::Cast { .. } | Expr::Convert { .. } | Expr::Function(_) - ) && self.consume_token(&Token::Period) - { - let tok = self.next_token(); - let name = match tok.token { - Token::Word(word) => word.to_ident(), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.location - ) - } + // Composite function chain + while matches!( + expr, + Expr::Function(_) + | Expr::CompositeFunction { .. } + | Expr::Cast { .. } + | Expr::Convert { .. } + ) && self.consume_token(&Token::Period) + { + let tok = self.next_token(); + let name = match tok.token { + Token::Word(word) => word.to_ident(), + _ => { + return parser_err!(format!("Expected identifier, found: {tok}"), tok.location) + } + }; + if self.consume_token(&Token::LParen) { + self.prev_token(); + let func = match self.parse_function(ObjectName(vec![name]))? { + Expr::Function(func) => func, + _ => unreachable!(), }; - self.expect_token(&Token::LParen)?; - let args = self.parse_comma_separated(Parser::parse_expr)?; - self.expect_token(&Token::RParen)?; - return Ok(Expr::CompositeFunction { - expr: Box::new(expr), - name, - args, + expr = Expr::CompositeFunction(CompositeFunction { + left: Box::new(expr), + right: func, }); } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4016e5a69..77cf6b8a5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11379,6 +11379,284 @@ fn test_try_convert() { dialects.verified_expr("TRY_CONVERT('foo', VARCHAR(MAX))"); } +#[test] +fn parse_composite_function() { + let sql = all_dialects().verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); + assert_eq!( + vec![SelectItem::ExprWithAlias { + expr: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "STUFF".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompositeFunction( + CompositeFunction { + left: Expr::Subquery( + Query { + with: None, + body: SetExpr::Select( + Select { + distinct: None, + top: None, + projection: vec![SelectItem::UnnamedExpr( + Expr::BinaryOp { + left: Expr::Value( + Value::SingleQuotedString(",".into()) + ) + .into(), + op: BinaryOperator::Plus, + right: Expr::Identifier(Ident { + value: "name".into(), + quote_style: None + }) + .into() + } + )], + into: None, + from: vec![TableWithJoins { + relation: TableFactor::Table { + name: ObjectName(vec![ + Ident { + value: "sys".into(), + quote_style: None, + }, + Ident { + value: "objects".into(), + quote_style: None, + } + ]), + alias: None, + args: None, + with_hints: vec![], + version: None, + with_ordinality: false, + partitions: vec![] + }, + joins: vec![] + }], + lateral_views: vec![], + prewhere: None, + selection: None, + group_by: GroupByExpr::Expressions(vec![], vec![]), + cluster_by: vec![], + distribute_by: vec![], + sort_by: vec![], + having: None, + named_window: vec![], + qualify: None, + window_before_qualify: false, + value_table_mode: None, + connect_by: None, + } + .into() + ) + .into(), + order_by: None, + limit: None, + limit_by: vec![], + offset: None, + fetch: None, + locks: vec![], + for_clause: Some(ForClause::Xml { + for_xml: ForXml::Path(Some("".into())), + elements: false, + binary_base64: false, + root: None, + r#type: true + }), + settings: None, + format_clause: None, + } + .into() + ) + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Value(Value::SingleQuotedString(".".into())) + )), + FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Value(Value::SingleQuotedString( + "NVARCHAR(MAX)".into() + )) + )) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + } + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }) + .into(), + alias: Ident { + value: "T".into(), + quote_style: None + } + }], + sql.projection + ); + let sql = all_dialects() + .verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); + assert_eq!( + vec![SelectItem::ExprWithAlias { + expr: Expr::CompositeFunction(CompositeFunction { + left: Expr::Cast { + kind: CastKind::Cast, + expr: Expr::Identifier(Ident { + value: "column".into(), + quote_style: None + }) + .into(), + data_type: DataType::Custom( + ObjectName(vec![Ident { + value: "XML".into(), + quote_style: None + }]), + vec![] + ), + format: None + } + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + }) + .into(), + alias: Ident { + value: "T".into(), + quote_style: None + } + }], + sql.projection + ); + let dialects = + all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); + let sql = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); + assert_eq!( + vec![SelectItem::ExprWithAlias { + expr: Expr::CompositeFunction(CompositeFunction { + left: Expr::CompositeFunction(CompositeFunction { + left: Expr::Convert { + expr: Expr::Value(Value::SingleQuotedString("abc".into())) + .into(), + is_try: false, + data_type: Some(DataType::Custom( + ObjectName(vec![Ident { + value: "XML".into(), + quote_style: None + }]), + vec![] + )), + charset: None, + target_before_value: true, + styles: vec![] + } + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + }) + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + }), + alias: Ident { + value: "T".into(), + quote_style: None + } + }], + sql.projection + ); +} + #[test] fn test_show_dbs_schemas_tables_views() { verified_stmt("SHOW DATABASES"); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7d70e6126..0223e2915 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1029,16 +1029,6 @@ fn parse_create_table_with_identity_column() { } } -#[test] -fn parse_mssql_xquery() { - let sql = "SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"; - let _ = ms().verified_query(sql); - let sql = "SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"; - let _ = ms().verified_query(sql); - let sql = "SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)') AS T"; - let _ = ms().verified_query(sql); -} - fn ms() -> TestedDialects { TestedDialects::new(vec![Box::new(MsSqlDialect {})]) } From ece9e989ce0b9eb77b9629f31ea0111722466d10 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Thu, 7 Nov 2024 17:16:59 +0800 Subject: [PATCH 07/15] Add test case --- tests/sqlparser_common.rs | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 77cf6b8a5..be235c959 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11655,6 +11655,93 @@ fn parse_composite_function() { }], sql.projection ); + let sql = dialects.verified_only_select( + "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", + ); + assert_eq!( + vec![SelectItem::ExprWithAlias { + expr: Expr::CompositeFunction(CompositeFunction { + left: Expr::CompositeFunction(CompositeFunction { + left: Expr::Function(Function { + name: ObjectName(vec![Ident { + value: "LEFT".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("abc".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number( + "1" + )))), + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }) + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + }) + .into(), + right: Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + } + }), + alias: Ident { + value: "T".into(), + quote_style: None + } + }], + sql.projection + ); } #[test] From 4fa802e77a97cb6b3018763606fc9c9afd3aa393 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Thu, 7 Nov 2024 17:48:05 +0800 Subject: [PATCH 08/15] Sync upstream changes --- tests/sqlparser_common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 8d77167df..006d7e71d 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11410,6 +11410,7 @@ fn parse_composite_function() { Select { distinct: None, top: None, + top_before_distinct: false, projection: vec![SelectItem::UnnamedExpr( Expr::BinaryOp { left: Expr::Value( From 75e9ac09cd304fc31649228337907d685dd1c938 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Fri, 8 Nov 2024 18:14:39 +0800 Subject: [PATCH 09/15] Code review comments --- src/ast/mod.rs | 20 +- src/dialect/mod.rs | 9 + src/dialect/mssql.rs | 4 + src/parser/mod.rs | 107 ++++----- tests/sqlparser_common.rs | 444 +++++++++++++------------------------- 5 files changed, 230 insertions(+), 354 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eebfda106..b8c53939e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -808,7 +808,7 @@ pub enum Expr { }, /// Scalar function call e.g. `LEFT(foo, 5)` Function(Function), - /// CompositeFunction (function chain) + /// Arbitrary expr method call /// /// Syntax: /// @@ -820,9 +820,9 @@ pub enum Expr { /// /// ```sql /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') - /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)') + /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') /// ``` - CompositeFunction(CompositeFunction), + Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// /// Note we only recognize a complete single expression as ``, @@ -1479,7 +1479,7 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{fun}"), - Expr::CompositeFunction(fun) => write!(f, "{fun}"), + Expr::Method(fun) => write!(f, "{fun}"), Expr::Case { operand, conditions, @@ -5609,18 +5609,18 @@ impl fmt::Display for FunctionArgumentClause { } } -/// A composite function call +/// A method call #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct CompositeFunction { - pub left: Box, - pub right: Function, +pub struct Method { + pub expr: Box, + pub method: Function, } -impl fmt::Display for CompositeFunction { +impl fmt::Display for Method { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}.{}", self.left, self.right,) + write!(f, "{}.{}", self.expr, self.method,) } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 453fee3de..0403c58e6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -279,6 +279,15 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports method calls, for example: + /// + /// ```sql + /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') + /// ``` + fn supports_methods(&self) -> bool { + false + } + /// Returns true if the dialect supports multiple variable assignment /// using parentheses in a `SET` variable declaration. /// diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index a5ee0bf75..a65421f7e 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -57,4 +57,8 @@ impl Dialect for MsSqlDialect { fn supports_try_convert(&self) -> bool { true } + + fn supports_methods(&self) -> bool { + true + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 838ce6a61..f83ea1a99 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1258,30 +1258,22 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - if !self.consume_token(&Token::Period) { + if let Some(expr) = self.try_parse_method(&expr)? { Ok(expr) } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.to_ident(), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.location - ) - } - }; - if self.consume_token(&Token::LParen) { - self.prev_token(); - let func = match self.parse_function(ObjectName(vec![key]))? { - Expr::Function(func) => func, - _ => unreachable!(), - }; - Ok(Expr::CompositeFunction(CompositeFunction { - left: Box::new(expr), - right: func, - })) + if !self.consume_token(&Token::Period) { + Ok(expr) } else { + let tok = self.next_token(); + let key = match tok.token { + Token::Word(word) => word.to_ident(), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.location + ) + } + }; Ok(Expr::CompositeAccess { expr: Box::new(expr), key, @@ -1300,33 +1292,9 @@ impl<'a> Parser<'a> { _ => self.expected("an expression", next_token), }?; - // Composite function chain - while matches!( - expr, - Expr::Function(_) - | Expr::CompositeFunction { .. } - | Expr::Cast { .. } - | Expr::Convert { .. } - ) && self.consume_token(&Token::Period) - { - let tok = self.next_token(); - let name = match tok.token { - Token::Word(word) => word.to_ident(), - _ => { - return parser_err!(format!("Expected identifier, found: {tok}"), tok.location) - } - }; - if self.consume_token(&Token::LParen) { - self.prev_token(); - let func = match self.parse_function(ObjectName(vec![name]))? { - Expr::Function(func) => func, - _ => unreachable!(), - }; - expr = Expr::CompositeFunction(CompositeFunction { - left: Box::new(expr), - right: func, - }); - } + // parse method calls + if let Some(method) = self.try_parse_method(&expr)? { + expr = method; } if self.parse_keyword(Keyword::COLLATE) { @@ -1386,6 +1354,45 @@ impl<'a> Parser<'a> { }) } + fn try_parse_method(&mut self, expr: &Expr) -> Result, ParserError> { + if !self.dialect.supports_methods() { + return Ok(None); + } + self.maybe_parse(|p| { + let mut method = None; + while p.consume_token(&Token::Period) { + let tok = p.next_token(); + let name = match tok.token { + Token::Word(word) => word.to_ident(), + _ => return p.expected("identifier", tok), + }; + let func = match p.parse_function(ObjectName(vec![name]))? { + Expr::Function(func) => func, + _ => return p.expected("function", p.peek_token()), + }; + match method.take() { + Some(expr) => { + method = Some(Expr::Method(Method { + expr: Box::new(expr), + method: func, + })); + } + None => { + method = Some(Expr::Method(Method { + expr: Box::new(expr.clone()), + method: func, + })) + } + } + } + if let Some(method) = method { + Ok(method) + } else { + p.expected("method", p.peek_token()) + } + }) + } + pub fn parse_function(&mut self, name: ObjectName) -> Result { self.expect_token(&Token::LParen)?; @@ -3573,9 +3580,9 @@ impl<'a> Parser<'a> { } /// Run a parser method `f`, reverting back to the current position if unsuccessful. - pub fn maybe_parse(&mut self, mut f: F) -> Result, ParserError> + pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> where - F: FnMut(&mut Parser) -> Result, + F: FnOnce(&mut Parser) -> Result, { let index = self.index; match f(self) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 006d7e71d..aaa7c57af 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11388,172 +11388,62 @@ fn test_try_convert() { } #[test] -fn parse_composite_function() { - let sql = all_dialects().verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); - assert_eq!( - vec![SelectItem::ExprWithAlias { - expr: Expr::Function(Function { - name: ObjectName(vec![Ident { - value: "STUFF".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::CompositeFunction( - CompositeFunction { - left: Expr::Subquery( - Query { - with: None, - body: SetExpr::Select( - Select { - distinct: None, - top: None, - top_before_distinct: false, - projection: vec![SelectItem::UnnamedExpr( - Expr::BinaryOp { - left: Expr::Value( - Value::SingleQuotedString(",".into()) - ) - .into(), - op: BinaryOperator::Plus, - right: Expr::Identifier(Ident { - value: "name".into(), - quote_style: None - }) - .into() - } - )], - into: None, - from: vec![TableWithJoins { - relation: TableFactor::Table { - name: ObjectName(vec![ - Ident { - value: "sys".into(), - quote_style: None, - }, - Ident { - value: "objects".into(), - quote_style: None, - } - ]), - alias: None, - args: None, - with_hints: vec![], - version: None, - with_ordinality: false, - partitions: vec![] - }, - joins: vec![] - }], - lateral_views: vec![], - prewhere: None, - selection: None, - group_by: GroupByExpr::Expressions(vec![], vec![]), - cluster_by: vec![], - distribute_by: vec![], - sort_by: vec![], - having: None, - named_window: vec![], - qualify: None, - window_before_qualify: false, - value_table_mode: None, - connect_by: None, - } - .into() - ) - .into(), - order_by: None, - limit: None, - limit_by: vec![], - offset: None, - fetch: None, - locks: vec![], - for_clause: Some(ForClause::Xml { - for_xml: ForXml::Path(Some("".into())), - elements: false, - binary_base64: false, - root: None, - r#type: true - }), - settings: None, - format_clause: None, - } - .into() - ) - .into(), - right: Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr( - Expr::Value(Value::SingleQuotedString(".".into())) - )), - FunctionArg::Unnamed(FunctionArgExpr::Expr( - Expr::Value(Value::SingleQuotedString( - "NVARCHAR(MAX)".into() - )) - )) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - } - } - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number("1")))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }) - .into(), - alias: Ident { - value: "T".into(), - quote_style: None - } - }], - sql.projection +fn parse_method_select() { + let dialects = all_dialects_where(|d| d.supports_methods()); + let _ = dialects.verified_only_select( + "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", ); - let sql = all_dialects() + let _ = dialects.verified_only_select("SELECT STUFF((SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS T"); + let _ = dialects .verified_only_select("SELECT CAST(column AS XML).value('.', 'NVARCHAR(MAX)') AS T"); - assert_eq!( - vec![SelectItem::ExprWithAlias { - expr: Expr::CompositeFunction(CompositeFunction { - left: Expr::Cast { - kind: CastKind::Cast, - expr: Expr::Identifier(Ident { - value: "column".into(), - quote_style: None - }) - .into(), - data_type: DataType::Custom( - ObjectName(vec![Ident { - value: "XML".into(), - quote_style: None - }]), - vec![] - ), - format: None + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let _ = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); +} + +#[test] +fn parse_method_expr() { + let dialects = all_dialects_where(|d| d.supports_methods()); + let expr = dialects + .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); + match expr { + Expr::Method(Method { expr, method }) => { + match *expr { + Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Function(_)) => { + assert_eq!( + Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }, + method + ); } - .into(), - right: Function { + _ => unreachable!(), + } + assert_eq!( + Function { name: ObjectName(vec![Ident { value: "value".into(), quote_style: None @@ -11575,65 +11465,19 @@ fn parse_composite_function() { null_treatment: None, over: None, within_group: vec![] - } - }) - .into(), - alias: Ident { - value: "T".into(), - quote_style: None - } - }], - sql.projection + }, + method + ); + } + _ => unreachable!(), + } + let expr = dialects.verified_expr( + "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", ); - let dialects = - all_dialects_where(|d| d.supports_try_convert() && d.convert_type_before_value()); - let sql = dialects.verified_only_select("SELECT CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T"); - assert_eq!( - vec![SelectItem::ExprWithAlias { - expr: Expr::CompositeFunction(CompositeFunction { - left: Expr::CompositeFunction(CompositeFunction { - left: Expr::Convert { - expr: Expr::Value(Value::SingleQuotedString("abc".into())) - .into(), - is_try: false, - data_type: Some(DataType::Custom( - ObjectName(vec![Ident { - value: "XML".into(), - quote_style: None - }]), - vec![] - )), - charset: None, - target_before_value: true, - styles: vec![] - } - .into(), - right: Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - } - }) - .into(), - right: Function { + match expr { + Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Subquery(_)) => { + assert_eq!( + Function { name: ObjectName(vec![Ident { value: "value".into(), quote_style: None @@ -11655,72 +11499,17 @@ fn parse_composite_function() { null_treatment: None, over: None, within_group: vec![] - } - }), - alias: Ident { - value: "T".into(), - quote_style: None - } - }], - sql.projection - ); - let sql = dialects.verified_only_select( - "SELECT LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)') AS T", - ); - assert_eq!( - vec![SelectItem::ExprWithAlias { - expr: Expr::CompositeFunction(CompositeFunction { - left: Expr::CompositeFunction(CompositeFunction { - left: Expr::Function(Function { - name: ObjectName(vec![Ident { - value: "LEFT".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("abc".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(number( - "1" - )))), - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }) - .into(), - right: Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - } - }) - .into(), - right: Function { + }, + method + ); + } + _ => unreachable!(), + } + let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); + match expr { + Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Cast { .. }) => { + assert_eq!( + Function { name: ObjectName(vec![Ident { value: "value".into(), quote_style: None @@ -11742,15 +11531,82 @@ fn parse_composite_function() { null_treatment: None, over: None, within_group: vec![] + }, + method + ); + } + _ => unreachable!(), + } + + // `CONVERT` support + let dialects = all_dialects_where(|d| { + d.supports_methods() && d.supports_try_convert() && d.convert_type_before_value() + }); + let expr = dialects.verified_expr( + "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", + ); + match expr { + Expr::Method(Method { expr, method }) => { + match *expr { + Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Convert { .. }) => { + assert_eq!( + Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }, + method + ); } - }), - alias: Ident { - value: "T".into(), - quote_style: None + _ => unreachable!(), } - }], - sql.projection - ); + assert_eq!( + Function { + name: ObjectName(vec![Ident { + value: "value".into(), + quote_style: None + }]), + parameters: FunctionArguments::None, + args: FunctionArguments::List(FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString(".".into()) + ))), + FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("NVARCHAR(MAX)".into()) + ))) + ], + clauses: vec![] + }), + filter: None, + null_treatment: None, + over: None, + within_group: vec![] + }, + method + ); + } + _ => unreachable!(), + } } #[test] From 90e48a4c63b05c4fb067434d0f2b3da7b7cf2875 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Fri, 8 Nov 2024 18:15:59 +0800 Subject: [PATCH 10/15] rename --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index b8c53939e..c2d19ecd0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1479,7 +1479,7 @@ impl fmt::Display for Expr { write!(f, " '{}'", &value::escape_single_quote_string(value)) } Expr::Function(fun) => write!(f, "{fun}"), - Expr::Method(fun) => write!(f, "{fun}"), + Expr::Method(method) => write!(f, "{method}"), Expr::Case { operand, conditions, From 9043ba7714e6596968338bef7563482359874961 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Fri, 8 Nov 2024 18:39:42 +0800 Subject: [PATCH 11/15] Code review comments --- tests/sqlparser_common.rs | 202 ++++++-------------------------------- 1 file changed, 28 insertions(+), 174 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aaa7c57af..af4ecf451 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11410,131 +11410,34 @@ fn parse_method_expr() { let expr = dialects .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method }) => { - match *expr { - Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Function(_)) => { - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } - _ => unreachable!(), - } - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } + Expr::Method(Method { + expr, + method: Function { .. }, + }) => match *expr { + Expr::Method(Method { + expr, + method: Function { .. }, + }) if matches!(*expr, Expr::Function(_)) => {} + _ => unreachable!(), + }, _ => unreachable!(), } let expr = dialects.verified_expr( "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Subquery(_)) => { - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } + Expr::Method(Method { + expr, + method: Function { .. }, + }) if matches!(*expr, Expr::Subquery(_)) => {} _ => unreachable!(), } let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Cast { .. }) => { - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } + Expr::Method(Method { + expr, + method: Function { .. }, + }) if matches!(*expr, Expr::Cast { .. }) => {} _ => unreachable!(), } @@ -11546,65 +11449,16 @@ fn parse_method_expr() { "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { expr, method }) => { - match *expr { - Expr::Method(Method { expr, method }) if matches!(*expr, Expr::Convert { .. }) => { - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } - _ => unreachable!(), - } - assert_eq!( - Function { - name: ObjectName(vec![Ident { - value: "value".into(), - quote_style: None - }]), - parameters: FunctionArguments::None, - args: FunctionArguments::List(FunctionArgumentList { - duplicate_treatment: None, - args: vec![ - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString(".".into()) - ))), - FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( - Value::SingleQuotedString("NVARCHAR(MAX)".into()) - ))) - ], - clauses: vec![] - }), - filter: None, - null_treatment: None, - over: None, - within_group: vec![] - }, - method - ); - } + Expr::Method(Method { + expr, + method: Function { .. }, + }) => match *expr { + Expr::Method(Method { + expr, + method: Function { .. }, + }) if matches!(*expr, Expr::Convert { .. }) => {} + _ => unreachable!(), + }, _ => unreachable!(), } } From cca523d77de0ad1faec8598c5fb6ee371ad5d4e8 Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Sat, 9 Nov 2024 13:34:57 +0800 Subject: [PATCH 12/15] Code review comments --- src/ast/mod.rs | 12 +++- src/parser/mod.rs | 112 ++++++++++++++++++-------------------- tests/sqlparser_common.rs | 50 ++++++++--------- 3 files changed, 84 insertions(+), 90 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c2d19ecd0..eef8397aa 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -812,7 +812,7 @@ pub enum Expr { /// /// Syntax: /// - /// `.....` + /// `.....` /// /// > `arbitrary-expr` can be any expression including a function call. /// @@ -5615,12 +5615,18 @@ impl fmt::Display for FunctionArgumentClause { #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct Method { pub expr: Box, - pub method: Function, + // always non-empty + pub method_chain: Vec, } impl fmt::Display for Method { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}.{}", self.expr, self.method,) + write!( + f, + "{}.{}", + self.expr, + display_separated(&self.method_chain, ".") + ) } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f83ea1a99..a185c906f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1012,7 +1012,7 @@ impl<'a> Parser<'a> { } let next_token = self.next_token(); - let mut expr = match next_token.token { + let expr = match next_token.token { Token::Word(w) => match w.keyword { Keyword::TRUE | Keyword::FALSE | Keyword::NULL => { self.prev_token(); @@ -1258,26 +1258,27 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - if let Some(expr) = self.try_parse_method(&expr)? { - Ok(expr) - } else { - if !self.consume_token(&Token::Period) { - Ok(expr) - } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.to_ident(), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.location - ) - } - }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) + match self.try_parse_method(expr)? { + Ok(expr) => Ok(expr), + Err(expr) => { + if !self.consume_token(&Token::Period) { + Ok(expr) + } else { + let tok = self.next_token(); + let key = match tok.token { + Token::Word(word) => word.to_ident(), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.location + ) + } + }; + Ok(Expr::CompositeAccess { + expr: Box::new(expr), + key, + }) + } } } } @@ -1292,18 +1293,18 @@ impl<'a> Parser<'a> { _ => self.expected("an expression", next_token), }?; - // parse method calls - if let Some(method) = self.try_parse_method(&expr)? { - expr = method; - } - - if self.parse_keyword(Keyword::COLLATE) { - Ok(Expr::Collate { - expr: Box::new(expr), - collation: self.parse_object_name(false)?, - }) - } else { - Ok(expr) + match self.try_parse_method(expr)? { + Ok(expr) => Ok(expr), + Err(expr) => { + if self.parse_keyword(Keyword::COLLATE) { + Ok(Expr::Collate { + expr: Box::new(expr), + collation: self.parse_object_name(false)?, + }) + } else { + Ok(expr) + } + } } } @@ -1354,12 +1355,15 @@ impl<'a> Parser<'a> { }) } - fn try_parse_method(&mut self, expr: &Expr) -> Result, ParserError> { + /// Parses method call expression + /// + /// Returns `Result<{parsed method-call-expr},{orig_expr}>` + fn try_parse_method(&mut self, orig_expr: Expr) -> Result, ParserError> { if !self.dialect.supports_methods() { - return Ok(None); + return Ok(Err(orig_expr)); } - self.maybe_parse(|p| { - let mut method = None; + let method_chain = self.maybe_parse(|p| { + let mut method_chain = Vec::new(); while p.consume_token(&Token::Period) { let tok = p.next_token(); let name = match tok.token { @@ -1370,27 +1374,17 @@ impl<'a> Parser<'a> { Expr::Function(func) => func, _ => return p.expected("function", p.peek_token()), }; - match method.take() { - Some(expr) => { - method = Some(Expr::Method(Method { - expr: Box::new(expr), - method: func, - })); - } - None => { - method = Some(Expr::Method(Method { - expr: Box::new(expr.clone()), - method: func, - })) - } - } - } - if let Some(method) = method { - Ok(method) - } else { - p.expected("method", p.peek_token()) + method_chain.push(func); } - }) + Ok(method_chain) + })?; + match method_chain { + Some(method_chain) if !method_chain.is_empty() => Ok(Ok(Expr::Method(Method { + expr: Box::new(orig_expr), + method_chain, + }))), + _ => Ok(Err(orig_expr)), + } } pub fn parse_function(&mut self, name: ObjectName) -> Result { @@ -3580,9 +3574,9 @@ impl<'a> Parser<'a> { } /// Run a parser method `f`, reverting back to the current position if unsuccessful. - pub fn maybe_parse(&mut self, f: F) -> Result, ParserError> + pub fn maybe_parse(&mut self, mut f: F) -> Result, ParserError> where - F: FnOnce(&mut Parser) -> Result, + F: FnMut(&mut Parser) -> Result, { let index = self.index; match f(self) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index af4ecf451..e4741e74b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11410,34 +11410,31 @@ fn parse_method_expr() { let expr = dialects .verified_expr("LEFT('abc', 1).value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) => match *expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) if matches!(*expr, Expr::Function(_)) => {} - _ => unreachable!(), - }, + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Function(_))); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } _ => unreachable!(), } let expr = dialects.verified_expr( "(SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) if matches!(*expr, Expr::Subquery(_)) => {} + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Subquery(_))); + assert!(matches!(method_chain[..], [Function { .. }])); + } _ => unreachable!(), } let expr = dialects.verified_expr("CAST(column AS XML).value('.', 'NVARCHAR(MAX)')"); match expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) if matches!(*expr, Expr::Cast { .. }) => {} + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Cast { .. })); + assert!(matches!(method_chain[..], [Function { .. }])); + } _ => unreachable!(), } @@ -11449,16 +11446,13 @@ fn parse_method_expr() { "CONVERT(XML, 'abc').value('.', 'NVARCHAR(MAX)').value('.', 'NVARCHAR(MAX)')", ); match expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) => match *expr { - Expr::Method(Method { - expr, - method: Function { .. }, - }) if matches!(*expr, Expr::Convert { .. }) => {} - _ => unreachable!(), - }, + Expr::Method(Method { expr, method_chain }) => { + assert!(matches!(*expr, Expr::Convert { .. })); + assert!(matches!( + method_chain[..], + [Function { .. }, Function { .. }] + )); + } _ => unreachable!(), } } From 8064f88d968cdf81b9bae8665be69e75dd0b3afc Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Sat, 9 Nov 2024 17:18:35 +0800 Subject: [PATCH 13/15] Code review comments --- src/ast/mod.rs | 2 ++ src/parser/mod.rs | 73 +++++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eef8397aa..933b22930 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -822,6 +822,8 @@ pub enum Expr { /// SELECT (SELECT ',' + name FROM sys.objects FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)') /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') /// ``` + /// + /// (mssql): https://learn.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver16 Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` /// diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a185c906f..ba355d141 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1258,28 +1258,24 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::RParen)?; - match self.try_parse_method(expr)? { - Ok(expr) => Ok(expr), - Err(expr) => { - if !self.consume_token(&Token::Period) { - Ok(expr) - } else { - let tok = self.next_token(); - let key = match tok.token { - Token::Word(word) => word.to_ident(), - _ => { - return parser_err!( - format!("Expected identifier, found: {tok}"), - tok.location - ) - } - }; - Ok(Expr::CompositeAccess { - expr: Box::new(expr), - key, - }) + let expr = self.try_parse_method(expr)?; + if !self.consume_token(&Token::Period) { + Ok(expr) + } else { + let tok = self.next_token(); + let key = match tok.token { + Token::Word(word) => word.to_ident(), + _ => { + return parser_err!( + format!("Expected identifier, found: {tok}"), + tok.location + ) } - } + }; + Ok(Expr::CompositeAccess { + expr: Box::new(expr), + key, + }) } } Token::Placeholder(_) | Token::Colon | Token::AtSign => { @@ -1293,18 +1289,15 @@ impl<'a> Parser<'a> { _ => self.expected("an expression", next_token), }?; - match self.try_parse_method(expr)? { - Ok(expr) => Ok(expr), - Err(expr) => { - if self.parse_keyword(Keyword::COLLATE) { - Ok(Expr::Collate { - expr: Box::new(expr), - collation: self.parse_object_name(false)?, - }) - } else { - Ok(expr) - } - } + let expr = self.try_parse_method(expr)?; + + if self.parse_keyword(Keyword::COLLATE) { + Ok(Expr::Collate { + expr: Box::new(expr), + collation: self.parse_object_name(false)?, + }) + } else { + Ok(expr) } } @@ -1356,11 +1349,9 @@ impl<'a> Parser<'a> { } /// Parses method call expression - /// - /// Returns `Result<{parsed method-call-expr},{orig_expr}>` - fn try_parse_method(&mut self, orig_expr: Expr) -> Result, ParserError> { + fn try_parse_method(&mut self, expr: Expr) -> Result { if !self.dialect.supports_methods() { - return Ok(Err(orig_expr)); + return Ok(expr); } let method_chain = self.maybe_parse(|p| { let mut method_chain = Vec::new(); @@ -1379,11 +1370,11 @@ impl<'a> Parser<'a> { Ok(method_chain) })?; match method_chain { - Some(method_chain) if !method_chain.is_empty() => Ok(Ok(Expr::Method(Method { - expr: Box::new(orig_expr), + Some(method_chain) if !method_chain.is_empty() => Ok(Expr::Method(Method { + expr: Box::new(expr), method_chain, - }))), - _ => Ok(Err(orig_expr)), + })), + _ => Ok(expr), } } From 5a47cc8bde70d696821df3b6cfd9e8806942fd0a Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Sat, 9 Nov 2024 17:24:05 +0800 Subject: [PATCH 14/15] Code review comments --- src/parser/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ba355d141..3cf92a641 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1367,14 +1367,19 @@ impl<'a> Parser<'a> { }; method_chain.push(func); } - Ok(method_chain) + if !method_chain.is_empty() { + Ok(method_chain) + } else { + p.expected("function", p.peek_token()) + } })?; - match method_chain { - Some(method_chain) if !method_chain.is_empty() => Ok(Expr::Method(Method { + if let Some(method_chain) = method_chain { + Ok(Expr::Method(Method { expr: Box::new(expr), method_chain, - })), - _ => Ok(expr), + })) + } else { + Ok(expr) } } From b0c585788355a16a21ed650ef1a2b12af4cef5be Mon Sep 17 00:00:00 2001 From: gaoqiangz Date: Sun, 10 Nov 2024 00:31:40 +0800 Subject: [PATCH 15/15] fix CI --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 933b22930..d95180334 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -823,7 +823,7 @@ pub enum Expr { /// SELECT CONVERT(XML,'abc').value('.','NVARCHAR(MAX)').value('.','NVARCHAR(MAX)') /// ``` /// - /// (mssql): https://learn.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver16 + /// (mssql): Method(Method), /// `CASE [] WHEN THEN ... [ELSE ] END` ///