Skip to content

Commit 7d6d9d0

Browse files
author
longshan.lu
committed
feat: Implement BETWEEN expression support in SQL parser and planner, enhancing expression handling and type coercion for decimal comparisons
1 parent f2f824e commit 7d6d9d0

File tree

8 files changed

+159
-26
lines changed

8 files changed

+159
-26
lines changed

qurious/src/execution/session.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -385,26 +385,14 @@ mod tests {
385385
.sql(
386386
"
387387
select
388-
o_orderpriority,
389-
count(*) as order_count
388+
sum(l_extendedprice * l_discount) as revenue
390389
from
391-
orders
390+
lineitem
392391
where
393-
o_orderdate >= '1993-07-01'
394-
and o_orderdate < date '1993-07-01' + interval '3' month
395-
and exists (
396-
select
397-
*
398-
from
399-
lineitem
400-
where
401-
l_orderkey = o_orderkey
402-
and l_commitdate < l_receiptdate
403-
)
404-
group by
405-
o_orderpriority
406-
order by
407-
o_orderpriority;
392+
l_shipdate >= date '1994-01-01'
393+
and l_shipdate < date '1995-01-01'
394+
and l_discount between 0.06 - 0.01 and 0.06 + 0.01
395+
and l_quantity < 24;
408396
",
409397
)
410398
.unwrap();

qurious/src/planner/sql.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,24 @@ impl<'a> SqlQueryPlanner<'a> {
10211021
expr: Box::new(self.sql_to_expr(*left)?),
10221022
pattern: Box::new(self.sql_to_expr(*right)?),
10231023
})),
1024+
Expression::Between {
1025+
negated,
1026+
expr,
1027+
low,
1028+
high,
1029+
} => {
1030+
let expr = self.sql_to_expr(*expr)?;
1031+
let low = self.sql_to_expr(*low)?;
1032+
let high = self.sql_to_expr(*high)?;
1033+
1034+
if negated {
1035+
// `expr NOT BETWEEN low AND high` ==> (expr < low) OR (expr > high)
1036+
Ok(or(lt(expr.clone(), low), gt(expr, high)))
1037+
} else {
1038+
// `expr BETWEEN low AND high` ==> (expr >= low) AND (expr <= high)
1039+
Ok(and(gt_eq(expr.clone(), low), lt_eq(expr, high)))
1040+
}
1041+
}
10241042
Expression::Exists { subquery, negated } => Ok(LogicalExpr::Exists(Exists {
10251043
negated,
10261044
subquery: Box::new(self.new_context_scope(|planner| planner.select_to_plan(*subquery))?),

qurious/src/utils/type_coercion.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,32 @@ fn coercion_types(lhs: &DataType, op: &Operator, rhs: &DataType) -> Result<Binar
5858
rhs: rhs.clone(),
5959
ret: DataType::Boolean,
6060
}),
61-
// Decimal comparisons against integral types: cast integral to decimal with scale 0.
62-
(Decimal128(_, _), Int8 | Int16 | Int32 | Int64) => Ok(BinaryTypes {
63-
lhs: lhs.clone(),
64-
rhs: coerce_numeric_type_to_decimal(rhs)?,
61+
// Decimal comparisons against integral types: cast the integral side to the SAME decimal
62+
// precision/scale as the decimal side. Arrow doesn't allow comparing decimals with differing
63+
// precision/scale (e.g. Decimal128(15,2) < Decimal128(20,0)).
64+
(Decimal128(p, s), Int8 | Int16 | Int32 | Int64) => Ok(BinaryTypes {
65+
lhs: DataType::Decimal128(*p, *s),
66+
rhs: DataType::Decimal128(*p, *s),
6567
ret: DataType::Boolean,
6668
}),
67-
(Int8 | Int16 | Int32 | Int64, Decimal128(_, _)) => Ok(BinaryTypes {
68-
lhs: coerce_numeric_type_to_decimal(lhs)?,
69-
rhs: rhs.clone(),
69+
(Int8 | Int16 | Int32 | Int64, Decimal128(p, s)) => Ok(BinaryTypes {
70+
lhs: DataType::Decimal128(*p, *s),
71+
rhs: DataType::Decimal128(*p, *s),
72+
ret: DataType::Boolean,
73+
}),
74+
// Decimal comparisons against float types: cast float to the same decimal type as the decimal side.
75+
//
76+
// This is important for predicates like:
77+
// decimal_col BETWEEN 0.06 - 0.01 AND 0.06 + 0.01
78+
// where the RHS is parsed as Float64 literals.
79+
(Decimal128(p, s), Float32 | Float64) => Ok(BinaryTypes {
80+
lhs: DataType::Decimal128(*p, *s),
81+
rhs: DataType::Decimal128(*p, *s),
82+
ret: DataType::Boolean,
83+
}),
84+
(Float32 | Float64, Decimal128(p, s)) => Ok(BinaryTypes {
85+
lhs: DataType::Decimal128(*p, *s),
86+
rhs: DataType::Decimal128(*p, *s),
7087
ret: DataType::Boolean,
7188
}),
7289
// Fallback: keep as-is (may error later if Arrow can't compare them)

qurious/tests/tpch/q5.slt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
query TR
2+
select
3+
n_name,
4+
sum(l_extendedprice * (1 - l_discount)) as revenue
5+
from
6+
customer,
7+
orders,
8+
lineitem,
9+
supplier,
10+
nation,
11+
region
12+
where
13+
c_custkey = o_custkey
14+
and l_orderkey = o_orderkey
15+
and l_suppkey = s_suppkey
16+
and c_nationkey = s_nationkey
17+
and s_nationkey = n_nationkey
18+
and n_regionkey = r_regionkey
19+
and r_name = 'ASIA'
20+
and o_orderdate >= date '1994-01-01'
21+
and o_orderdate < date '1995-01-01'
22+
group by
23+
n_name
24+
order by
25+
revenue desc;
26+
----
27+
VIETNAM 1000926.6999
28+
CHINA 740210.7570
29+
JAPAN 660651.2425
30+
INDONESIA 566379.5276
31+
INDIA 422874.6844

qurious/tests/tpch/q6.slt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
query R
2+
select
3+
sum(l_extendedprice * l_discount) as revenue
4+
from
5+
lineitem
6+
where
7+
l_shipdate >= date '1994-01-01'
8+
and l_shipdate < date '1995-01-01'
9+
and l_discount between 0.06 - 0.01 and 0.06 + 0.01
10+
and l_quantity < 24;
11+
----
12+
1193053.2253

sqlparser/src/ast.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,13 @@ pub enum Expression {
673673
left: Box<Expression>,
674674
right: Box<Expression>,
675675
},
676+
/// `[ NOT ] BETWEEN <low> AND <high>`
677+
Between {
678+
negated: bool,
679+
expr: Box<Expression>,
680+
low: Box<Expression>,
681+
high: Box<Expression>,
682+
},
676683
/// `INTERVAL <expr> <field>`
677684
Interval {
678685
expr: Box<Expression>,
@@ -811,6 +818,21 @@ impl Display for Expression {
811818
Expression::Like { negated, left, right } => {
812819
write!(f, "{} {} LIKE {}", left, if *negated { "NOT" } else { "" }, right)
813820
}
821+
Expression::Between {
822+
negated,
823+
expr,
824+
low,
825+
high,
826+
} => {
827+
write!(
828+
f,
829+
"{} {} BETWEEN {} AND {}",
830+
expr,
831+
if *negated { "NOT" } else { "" },
832+
low,
833+
high
834+
)
835+
}
814836
Expression::Interval { expr, field } => write!(f, "INTERVAL {} {}", expr, field),
815837
Expression::Exists { subquery, negated } => {
816838
write!(f, "{} EXISTS ({})", if *negated { "NOT" } else { "" }, subquery)

sqlparser/src/parser.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,18 @@ impl<'a> Parser<'a> {
875875
left: Box::new(lhs),
876876
right: self.parse_expression(infix.precedence()).map(Box::new)?,
877877
},
878+
InfixOperator::Between => {
879+
let low = self.parse_expression(infix.precedence())?;
880+
self.next_except(TokenType::Keyword(Keyword::And))?;
881+
let high = self.parse_expression(infix.precedence())?;
882+
883+
Expression::Between {
884+
negated,
885+
expr: Box::new(lhs),
886+
low: Box::new(low),
887+
high: Box::new(high),
888+
}
889+
}
878890
InfixOperator::In => self.parse_in_expr(lhs, negated)?,
879891
InfixOperator::DoubleColon => self.parse_data_type().map(|dt| Expression::Cast {
880892
expr: Box::new(lhs),
@@ -1253,6 +1265,7 @@ enum InfixOperator {
12531265
And,
12541266
Or,
12551267
In,
1268+
Between,
12561269
DoubleColon,
12571270
Is,
12581271
Like,
@@ -1275,6 +1288,7 @@ impl Operator for InfixOperator {
12751288
TokenType::Keyword(Keyword::And) => Some(InfixOperator::And),
12761289
TokenType::Keyword(Keyword::Or) => Some(InfixOperator::Or),
12771290
TokenType::Keyword(Keyword::In) => Some(InfixOperator::In),
1291+
TokenType::Keyword(Keyword::Between) => Some(InfixOperator::Between),
12781292
TokenType::Keyword(Keyword::Is) => Some(InfixOperator::Is),
12791293
TokenType::Keyword(Keyword::Like) => Some(InfixOperator::Like),
12801294
_ => None,
@@ -1285,7 +1299,7 @@ impl Operator for InfixOperator {
12851299
match self {
12861300
InfixOperator::Or => 1,
12871301
InfixOperator::And => 2,
1288-
InfixOperator::Eq | InfixOperator::NotEq | InfixOperator::Like => 3,
1302+
InfixOperator::Eq | InfixOperator::NotEq | InfixOperator::Like | InfixOperator::Between => 3,
12891303
InfixOperator::Gt | InfixOperator::Gte | InfixOperator::Lt | InfixOperator::Lte => 4,
12901304
InfixOperator::Add | InfixOperator::Sub => 5,
12911305
InfixOperator::Mul | InfixOperator::Div => 6,
@@ -4584,6 +4598,35 @@ mod tests {
45844598
}
45854599
}
45864600

4601+
#[test]
4602+
fn test_parse_between_expression() {
4603+
let stmt = parse_stmt("SELECT * FROM tbl WHERE id BETWEEN 1 AND 3").unwrap();
4604+
4605+
assert_eq!(
4606+
stmt,
4607+
ast::Statement::Select(Box::new(ast::Select {
4608+
with: None,
4609+
distinct: None,
4610+
columns: vec![ast::SelectItem::Wildcard],
4611+
from: vec![ast::From::Table {
4612+
name: "tbl".to_owned(),
4613+
alias: None
4614+
}],
4615+
r#where: Some(ast::Expression::Between {
4616+
negated: false,
4617+
expr: Box::new(ast::Expression::Identifier("id".into())),
4618+
low: Box::new(ast::Expression::Literal(ast::Literal::Int(1))),
4619+
high: Box::new(ast::Expression::Literal(ast::Literal::Int(3))),
4620+
}),
4621+
group_by: None,
4622+
having: None,
4623+
order_by: None,
4624+
limit: None,
4625+
offset: None,
4626+
}))
4627+
);
4628+
}
4629+
45874630
fn parse_stmt(input: &str) -> Result<Statement> {
45884631
let mut parser = Parser::new(input);
45894632
parser.parse()

sqlparser/src/token.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub enum Keyword {
4141
Distinct,
4242
Having,
4343
In,
44+
Between,
4445
On,
4546
As,
4647
Is,
@@ -179,6 +180,7 @@ impl TokenType {
179180
"on" => TokenType::Keyword(Keyword::On),
180181
"set" => TokenType::Keyword(Keyword::Set),
181182
"in" => TokenType::Keyword(Keyword::In),
183+
"between" => TokenType::Keyword(Keyword::Between),
182184
"distinct" => TokenType::Keyword(Keyword::Distinct),
183185
"having" => TokenType::Keyword(Keyword::Having),
184186
"true" => TokenType::Keyword(Keyword::True),

0 commit comments

Comments
 (0)