Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1188,12 +1188,16 @@ pub enum Expr {
/// ```sql
/// SUBSTRING(<expr>, <expr>, <expr>)
/// ```
/// or
/// ```sql
/// SUBSTRING(<expr> SIMILAR <expr> ESCAPE <expr>)
/// ```
Substring {
/// Source expression.
expr: Box<Expr>,
/// Optional `FROM` expression.
/// Optional `FROM`/`SIMILAR` expression.
substring_from: Option<Box<Expr>>,
/// Optional `FOR` expression.
/// Optional `FOR`/`ESCAPE` expression.
substring_for: Option<Box<Expr>>,

/// false if the expression is represented using the `SUBSTRING(expr [FROM start] [FOR len])` syntax
Expand All @@ -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] [<expr> FROM] <expr>)
Expand Down Expand Up @@ -2135,6 +2144,7 @@ impl fmt::Display for Expr {
substring_for,
special,
shorthand,
similar,
} => {
f.write_str("SUBSTR")?;
if !*shorthand {
Expand All @@ -2144,13 +2154,17 @@ 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}")?;
}
}
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}")?;
}
Expand Down
1 change: 1 addition & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
28 changes: 27 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.postgresql.org/docs/current/functions-string.html>.
pub fn parse_substring(&mut self) -> Result<Expr, ParserError> {
let shorthand = match self.expect_one_of_keywords(&[Keyword::SUBSTR, Keyword::SUBSTRING])? {
Keyword::SUBSTR => true,
Expand All @@ -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) {
Expand All @@ -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 <count> FROM <start>`, 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)?;

Expand All @@ -3060,6 +3085,7 @@ impl<'a> Parser<'a> {
substring_for: to_expr.map(Box::new),
special,
shorthand,
similar: false,
})
}

Expand Down
5 changes: 5 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,7 @@ fn parse_substring_in_select() {
))),
special: true,
shorthand: false,
similar: false,
})],
exclude: None,
into: None,
Expand Down
1 change: 1 addition & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3369,6 +3369,7 @@ fn parse_substring_in_select() {
))),
special: true,
shorthand: false,
similar: false,
})],
exclude: None,
into: None,
Expand Down
Loading