Skip to content

Commit 8bbb853

Browse files
authored
Fix SUBSTRING from/to argument construction for mssql (#947)
1 parent 173a6db commit 8bbb853

File tree

7 files changed

+153
-24
lines changed

7 files changed

+153
-24
lines changed

src/ast/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ pub enum Expr {
477477
expr: Box<Expr>,
478478
substring_from: Option<Box<Expr>>,
479479
substring_for: Option<Box<Expr>>,
480+
481+
// Some dialects use `SUBSTRING(expr [FROM start] [FOR len])` syntax while others omit FROM,
482+
// FOR keywords (e.g. Microsoft SQL Server). This flags is used for formatting.
483+
special: bool,
480484
},
481485
/// ```sql
482486
/// TRIM([BOTH | LEADING | TRAILING] [<expr> FROM] <expr>)
@@ -830,13 +834,22 @@ impl fmt::Display for Expr {
830834
expr,
831835
substring_from,
832836
substring_for,
837+
special,
833838
} => {
834839
write!(f, "SUBSTRING({expr}")?;
835840
if let Some(from_part) = substring_from {
836-
write!(f, " FROM {from_part}")?;
841+
if *special {
842+
write!(f, ", {from_part}")?;
843+
} else {
844+
write!(f, " FROM {from_part}")?;
845+
}
837846
}
838847
if let Some(for_part) = substring_for {
839-
write!(f, " FOR {for_part}")?;
848+
if *special {
849+
write!(f, ", {for_part}")?;
850+
} else {
851+
write!(f, " FOR {for_part}")?;
852+
}
840853
}
841854

842855
write!(f, ")")

src/dialect/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ pub trait Dialect: Debug + Any {
117117
fn supports_group_by_expr(&self) -> bool {
118118
false
119119
}
120+
/// Returns true if the dialect supports `SUBSTRING(expr [FROM start] [FOR len])` expressions
121+
fn supports_substring_from_for_expr(&self) -> bool {
122+
true
123+
}
120124
/// Dialect-specific prefix parser override
121125
fn parse_prefix(&self, _parser: &mut Parser) -> Option<Result<Expr, ParserError>> {
122126
// return None to fall back to the default behavior

src/dialect/mssql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ impl Dialect for MsSqlDialect {
3434
|| ch == '#'
3535
|| ch == '_'
3636
}
37+
38+
fn supports_substring_from_for_expr(&self) -> bool {
39+
false
40+
}
3741
}

src/parser.rs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,25 +1223,47 @@ impl<'a> Parser<'a> {
12231223
}
12241224

12251225
pub fn parse_substring_expr(&mut self) -> Result<Expr, ParserError> {
1226-
// PARSE SUBSTRING (EXPR [FROM 1] [FOR 3])
1227-
self.expect_token(&Token::LParen)?;
1228-
let expr = self.parse_expr()?;
1229-
let mut from_expr = None;
1230-
if self.parse_keyword(Keyword::FROM) || self.consume_token(&Token::Comma) {
1231-
from_expr = Some(self.parse_expr()?);
1232-
}
1226+
if self.dialect.supports_substring_from_for_expr() {
1227+
// PARSE SUBSTRING (EXPR [FROM 1] [FOR 3])
1228+
self.expect_token(&Token::LParen)?;
1229+
let expr = self.parse_expr()?;
1230+
let mut from_expr = None;
1231+
if self.parse_keyword(Keyword::FROM) || self.consume_token(&Token::Comma) {
1232+
from_expr = Some(self.parse_expr()?);
1233+
}
12331234

1234-
let mut to_expr = None;
1235-
if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) {
1236-
to_expr = Some(self.parse_expr()?);
1237-
}
1238-
self.expect_token(&Token::RParen)?;
1235+
let mut to_expr = None;
1236+
if self.parse_keyword(Keyword::FOR) || self.consume_token(&Token::Comma) {
1237+
to_expr = Some(self.parse_expr()?);
1238+
}
1239+
self.expect_token(&Token::RParen)?;
12391240

1240-
Ok(Expr::Substring {
1241-
expr: Box::new(expr),
1242-
substring_from: from_expr.map(Box::new),
1243-
substring_for: to_expr.map(Box::new),
1244-
})
1241+
Ok(Expr::Substring {
1242+
expr: Box::new(expr),
1243+
substring_from: from_expr.map(Box::new),
1244+
substring_for: to_expr.map(Box::new),
1245+
special: !self.dialect.supports_substring_from_for_expr(),
1246+
})
1247+
} else {
1248+
// PARSE SUBSTRING(EXPR, start, length)
1249+
self.expect_token(&Token::LParen)?;
1250+
let expr = self.parse_expr()?;
1251+
1252+
self.expect_token(&Token::Comma)?;
1253+
let from_expr = Some(self.parse_expr()?);
1254+
1255+
self.expect_token(&Token::Comma)?;
1256+
let to_expr = Some(self.parse_expr()?);
1257+
1258+
self.expect_token(&Token::RParen)?;
1259+
1260+
Ok(Expr::Substring {
1261+
expr: Box::new(expr),
1262+
substring_from: from_expr.map(Box::new),
1263+
substring_for: to_expr.map(Box::new),
1264+
special: !self.dialect.supports_substring_from_for_expr(),
1265+
})
1266+
}
12451267
}
12461268

12471269
pub fn parse_overlay_expr(&mut self) -> Result<Expr, ParserError> {

tests/sqlparser_common.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5084,19 +5084,45 @@ fn parse_scalar_subqueries() {
50845084

50855085
#[test]
50865086
fn parse_substring() {
5087-
one_statement_parses_to("SELECT SUBSTRING('1')", "SELECT SUBSTRING('1')");
5087+
let from_for_supported_dialects = TestedDialects {
5088+
dialects: vec![
5089+
Box::new(GenericDialect {}),
5090+
Box::new(PostgreSqlDialect {}),
5091+
Box::new(AnsiDialect {}),
5092+
Box::new(SnowflakeDialect {}),
5093+
Box::new(HiveDialect {}),
5094+
Box::new(RedshiftSqlDialect {}),
5095+
Box::new(MySqlDialect {}),
5096+
Box::new(BigQueryDialect {}),
5097+
Box::new(SQLiteDialect {}),
5098+
Box::new(DuckDbDialect {}),
5099+
],
5100+
options: None,
5101+
};
50885102

5089-
one_statement_parses_to(
5103+
let from_for_unsupported_dialects = TestedDialects {
5104+
dialects: vec![Box::new(MsSqlDialect {})],
5105+
options: None,
5106+
};
5107+
5108+
from_for_supported_dialects
5109+
.one_statement_parses_to("SELECT SUBSTRING('1')", "SELECT SUBSTRING('1')");
5110+
5111+
from_for_supported_dialects.one_statement_parses_to(
50905112
"SELECT SUBSTRING('1' FROM 1)",
50915113
"SELECT SUBSTRING('1' FROM 1)",
50925114
);
50935115

5094-
one_statement_parses_to(
5116+
from_for_supported_dialects.one_statement_parses_to(
50955117
"SELECT SUBSTRING('1' FROM 1 FOR 3)",
50965118
"SELECT SUBSTRING('1' FROM 1 FOR 3)",
50975119
);
50985120

5099-
one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)");
5121+
from_for_unsupported_dialects
5122+
.one_statement_parses_to("SELECT SUBSTRING('1', 1, 3)", "SELECT SUBSTRING('1', 1, 3)");
5123+
5124+
from_for_supported_dialects
5125+
.one_statement_parses_to("SELECT SUBSTRING('1' FOR 3)", "SELECT SUBSTRING('1' FOR 3)");
51005126
}
51015127

51025128
#[test]

tests/sqlparser_mssql.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,65 @@ fn parse_similar_to() {
379379
chk(true);
380380
}
381381

382+
#[test]
383+
fn parse_substring_in_select() {
384+
let sql = "SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test";
385+
match ms().one_statement_parses_to(
386+
sql,
387+
"SELECT DISTINCT SUBSTRING(description, 0, 1) FROM test",
388+
) {
389+
Statement::Query(query) => {
390+
assert_eq!(
391+
Box::new(Query {
392+
with: None,
393+
body: Box::new(SetExpr::Select(Box::new(Select {
394+
distinct: Some(Distinct::Distinct),
395+
top: None,
396+
projection: vec![SelectItem::UnnamedExpr(Expr::Substring {
397+
expr: Box::new(Expr::Identifier(Ident {
398+
value: "description".to_string(),
399+
quote_style: None
400+
})),
401+
substring_from: Some(Box::new(Expr::Value(number("0")))),
402+
substring_for: Some(Box::new(Expr::Value(number("1")))),
403+
special: true,
404+
})],
405+
into: None,
406+
from: vec![TableWithJoins {
407+
relation: TableFactor::Table {
408+
name: ObjectName(vec![Ident {
409+
value: "test".to_string(),
410+
quote_style: None
411+
}]),
412+
alias: None,
413+
args: None,
414+
with_hints: vec![]
415+
},
416+
joins: vec![]
417+
}],
418+
lateral_views: vec![],
419+
selection: None,
420+
group_by: vec![],
421+
cluster_by: vec![],
422+
distribute_by: vec![],
423+
sort_by: vec![],
424+
having: None,
425+
named_window: vec![],
426+
qualify: None
427+
}))),
428+
order_by: vec![],
429+
limit: None,
430+
offset: None,
431+
fetch: None,
432+
locks: vec![],
433+
}),
434+
query
435+
);
436+
}
437+
_ => unreachable!(),
438+
}
439+
}
440+
382441
fn ms() -> TestedDialects {
383442
TestedDialects {
384443
dialects: vec![Box::new(MsSqlDialect {})],

tests/sqlparser_mysql.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,8 @@ fn parse_substring_in_select() {
12711271
quote_style: None
12721272
})),
12731273
substring_from: Some(Box::new(Expr::Value(number("0")))),
1274-
substring_for: Some(Box::new(Expr::Value(number("1"))))
1274+
substring_for: Some(Box::new(Expr::Value(number("1")))),
1275+
special: false,
12751276
})],
12761277
into: None,
12771278
from: vec![TableWithJoins {

0 commit comments

Comments
 (0)