diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a899d3ec3a..96df1fb3b1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1195,12 +1195,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 @@ -1211,6 +1215,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] ) @@ -2160,6 +2169,7 @@ impl fmt::Display for Expr { substring_for, special, shorthand, + similar, } => { f.write_str("SUBSTR")?; if !*shorthand { @@ -2169,6 +2179,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}")?; } @@ -2176,6 +2188,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 945cbb0985..499ac36d3f 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1634,6 +1634,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 b8238843a9..0b327eb68b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1432,6 +1432,10 @@ impl<'a> Parser<'a> { } debug!("prefix: {expr:?}"); + self.parse_infix_chain(expr, precedence) + } + + fn parse_infix_chain(&mut self, mut expr: Expr, precedence: u8) -> Result { loop { let next_precedence = self.get_next_precedence()?; debug!("next precedence: {next_precedence:?}"); @@ -3033,6 +3037,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, @@ -3043,7 +3050,30 @@ impl<'a> Parser<'a> { } }; self.expect_token(&Token::LParen)?; - let expr = self.parse_expr()?; + let mut expr = self.parse_subexpr(self.dialect.prec_value(Precedence::Like))?; + + if self.peek_keyword(Keyword::SIMILAR) + && !matches!( + &self.peek_nth_token_ref(1).token, + Token::Word(word) if word.keyword == Keyword::TO + ) + { + self.advance_token(); + 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, + }); + } + expr = self.parse_infix_chain(expr, self.dialect.prec_unknown())?; + let mut from_expr = None; let special = self.consume_token(&Token::Comma); if special || self.parse_keyword(Keyword::FROM) { @@ -3053,6 +3083,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)?; @@ -3062,6 +3096,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 354ccc8b75..408a2805b9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -8428,6 +8428,13 @@ 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(a IS NULL FROM 1)"); + verified_stmt("SELECT SUBSTRING(a LIKE 'x' FROM 1)"); + 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 d978f4a69a..29b2b67477 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 e3fa77f068..b38bad3295 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, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index fb99851077..c3ff64a085 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -10126,3 +10126,10 @@ fn parse_bitstring_literal_escaping() { pg_and_generic().verified_stmt("SELECT B''''"); pg_and_generic().verified_stmt("SELECT B'it''s'"); } + +#[test] +fn parse_substring_source_expression_operators() { + pg().verified_stmt("SELECT SUBSTRING(1 = 1 FROM 1)"); + pg().verified_stmt("SELECT SUBSTRING(a ~ 'x' FROM 1)"); + pg().verified_stmt("SELECT SUBSTRING(a SIMILAR TO 'x' FROM 1)"); +}