diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 20058b83a..e586ffe6d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1188,12 +1188,16 @@ pub enum Expr { /// ```sql /// SUBSTRING(, , ) /// ``` + /// or + /// ```sql + /// SUBSTRING( SIMILAR ESCAPE ) + /// ``` Substring { /// Source expression. expr: Box, - /// Optional `FROM` expression. + /// Optional `FROM`/`SIMILAR` expression. substring_from: Option>, - /// Optional `FOR` expression. + /// Optional `FOR`/`ESCAPE` expression. substring_for: Option>, /// false if the expression is represented using the `SUBSTRING(expr [FROM start] [FOR len])` syntax @@ -1204,6 +1208,11 @@ pub enum Expr { /// true if the expression is represented using the `SUBSTR` shorthand /// This flag is used for formatting. shorthand: bool, + + /// true if the expression uses the `SUBSTRING(expr SIMILAR pattern ESCAPE escape)` syntax, + /// in which case `substring_from` holds the pattern and `substring_for` holds the escape. + /// This flag is used for formatting. + similar: bool, }, /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) @@ -2135,6 +2144,7 @@ impl fmt::Display for Expr { substring_for, special, shorthand, + similar, } => { f.write_str("SUBSTR")?; if !*shorthand { @@ -2144,6 +2154,8 @@ impl fmt::Display for Expr { if let Some(from_part) = substring_from { if *special { write!(f, ", {from_part}")?; + } else if *similar { + write!(f, " SIMILAR {from_part}")?; } else { write!(f, " FROM {from_part}")?; } @@ -2151,6 +2163,8 @@ impl fmt::Display for Expr { if let Some(for_part) = substring_for { if *special { write!(f, ", {for_part}")?; + } else if *similar { + write!(f, " ESCAPE {for_part}")?; } else { write!(f, " FOR {for_part}")?; } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index cb06b7899..d36350d9a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1629,6 +1629,7 @@ impl Spanned for Expr { substring_for, special: _, shorthand: _, + similar: _, } => union_spans( core::iter::once(expr.span()) .chain(substring_from.as_ref().map(|i| i.span())) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c23b27053..6100f5b00 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3031,6 +3031,9 @@ impl<'a> Parser<'a> { } /// Parse `SUBSTRING`/`SUBSTR` expressions: `SUBSTRING(expr FROM start FOR length)` or `SUBSTR(expr, start, length)`. + /// Also supports the Postgres `SUBSTRING(expr SIMILAR pattern ESCAPE escape)` syntax and the + /// reversed `SUBSTRING(expr FOR length FROM start)` argument order. + /// See . pub fn parse_substring(&mut self) -> Result { let shorthand = match self.expect_one_of_keywords(&[Keyword::SUBSTR, Keyword::SUBSTRING])? { Keyword::SUBSTR => true, @@ -3041,7 +3044,25 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; + // Parse at `LIKE` precedence so a bare `SIMILAR` isn't swallowed as the start of a + // `SIMILAR TO` operator, allowing it to be recognized as the substring syntax below. + let expr = self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?; + + if self.parse_keyword(Keyword::SIMILAR) { + let from_expr = self.parse_expr()?; + self.expect_keyword_is(Keyword::ESCAPE)?; + let to_expr = self.parse_expr()?; + self.expect_token(&Token::RParen)?; + return Ok(Expr::Substring { + expr: Box::new(expr), + substring_from: Some(Box::new(from_expr)), + substring_for: Some(Box::new(to_expr)), + special: false, + shorthand, + similar: true, + }); + } + let mut from_expr = None; let special = self.consume_token(&Token::Comma); if special || self.parse_keyword(Keyword::FROM) { @@ -3051,6 +3072,10 @@ impl<'a> Parser<'a> { let mut to_expr = None; if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) { to_expr = Some(self.parse_expr()?); + // Postgres also allows `FOR FROM `, i.e. the reverse order. + if from_expr.is_none() && !special && self.parse_keyword(Keyword::FROM) { + from_expr = Some(self.parse_expr()?); + } } self.expect_token(&Token::RParen)?; @@ -3060,6 +3085,7 @@ impl<'a> Parser<'a> { substring_for: to_expr.map(Box::new), special, shorthand, + similar: false, }) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 29b060a82..db281c075 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8425,6 +8425,11 @@ fn parse_substring() { verified_stmt("SELECT SUBSTRING('foo' FROM 1 FOR 2) FROM t"); verified_stmt("SELECT SUBSTR('foo' FROM 1 FOR 2) FROM t"); verified_stmt("SELECT SUBSTR('foo', 1, 2) FROM t"); + verified_stmt("SELECT SUBSTRING('1' SIMILAR '_' ESCAPE '#')"); + one_statement_parses_to( + "SELECT SUBSTRING('1' FOR 3 FROM 1)", + "SELECT SUBSTRING('1' FROM 1 FOR 3)", + ); } #[test] diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 4510f953e..d5f188de4 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1370,6 +1370,7 @@ fn parse_substring_in_select() { ))), special: true, shorthand: false, + similar: false, })], exclude: None, into: None, diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 97f213743..0afa6617b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -3369,6 +3369,7 @@ fn parse_substring_in_select() { ))), special: true, shorthand: false, + similar: false, })], exclude: None, into: None,