From 2180ba04ec69efe01c673245238ff73cb5c21fe8 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 24 Feb 2026 16:23:27 +0800 Subject: [PATCH 1/2] feat: implement IS DISTINCT FROM expression --- pqb/src/expr.rs | 26 +++++++++++++++++++++++++- pqb/tests/expr.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pqb/src/expr.rs b/pqb/src/expr.rs index ac225e0..7bd53a4 100644 --- a/pqb/src/expr.rs +++ b/pqb/src/expr.rs @@ -378,6 +378,22 @@ impl Expr { self.binary(BinaryOp::IsNot, right) } + /// Express a `IS DISTINCT FROM` expression. + pub fn is_distinct_from(self, right: R) -> Expr + where + R: Into, + { + self.binary(BinaryOp::IsDistinctFrom, right) + } + + /// Express a `IS NOT DISTINCT FROM` expression. + pub fn is_not_distinct_from(self, right: R) -> Expr + where + R: Into, + { + self.binary(BinaryOp::IsNotDistinctFrom, right) + } + /// Express a `IN` expression. pub fn is_in(self, v: I) -> Expr where @@ -479,6 +495,8 @@ pub enum BinaryOp { NotLike, Is, IsNot, + IsDistinctFrom, + IsNotDistinctFrom, In, NotIn, LShift, @@ -645,6 +663,8 @@ fn write_binary_op(w: &mut W, op: &BinaryOp) { BinaryOp::NotLike => "NOT LIKE", BinaryOp::Is => "IS", BinaryOp::IsNot => "IS NOT", + BinaryOp::IsDistinctFrom => "IS DISTINCT FROM", + BinaryOp::IsNotDistinctFrom => "IS NOT DISTINCT FROM", BinaryOp::In => "IN", BinaryOp::NotIn => "NOT IN", BinaryOp::Between => "BETWEEN", @@ -740,6 +760,7 @@ fn well_known_high_precedence(expr: &Expr, outer_op: &Operator) -> bool { || outer_op.is_between() || outer_op.is_in() || outer_op.is_like() + || outer_op.is_is() || outer_op.is_logical(); } @@ -789,7 +810,10 @@ impl Operator { fn is_is(&self) -> bool { matches!( self, - Operator::Binary(BinaryOp::Is) | Operator::Binary(BinaryOp::IsNot) + Operator::Binary(BinaryOp::Is) + | Operator::Binary(BinaryOp::IsNot) + | Operator::Binary(BinaryOp::IsDistinctFrom) + | Operator::Binary(BinaryOp::IsNotDistinctFrom) ) } diff --git a/pqb/tests/expr.rs b/pqb/tests/expr.rs index cd7daed..262a1c0 100644 --- a/pqb/tests/expr.rs +++ b/pqb/tests/expr.rs @@ -57,3 +57,40 @@ fn select_range_ops() { @r#"SELECT * FROM "ranges" WHERE "r1" @> "r2" AND "r1" <@ "r2" AND "r1" && "r2" AND "r1" << "r2" AND "r1" >> "r2" AND "r1" &< "r2" AND "r1" &> "r2" AND "r1" -|- "r2""# ); } + +#[test] +fn select_is_distinct_from() { + let left = Expr::column("c1"); + let right = Expr::column("c2"); + + assert_snapshot!( + Select::new() + .expr(Expr::asterisk()) + .from("t") + .and_where(left.clone().is_distinct_from(right.clone())) + .and_where(left.clone().is_not_distinct_from(right.clone())) + .to_sql() + .validate(), + @r#"SELECT * FROM "t" WHERE "c1" IS DISTINCT FROM "c2" AND "c1" IS NOT DISTINCT FROM "c2""# + ); + + assert_snapshot!( + Select::new() + .expr(Expr::asterisk()) + .from("t") + .and_where(left.clone().add(Expr::value(1)).is_distinct_from(right.clone().add(Expr::value(2)))) + .to_sql() + .validate(), + @r#"SELECT * FROM "t" WHERE "c1" + 1 IS DISTINCT FROM "c2" + 2"# + ); + + assert_snapshot!( + Select::new() + .expr(Expr::asterisk()) + .from("t") + .and_where(left.clone().add(Expr::value(1)).is_null()) + .to_sql() + .validate(), + @r#"SELECT * FROM "t" WHERE "c1" + 1 IS NULL"# + ); +} From 651c91f2290691db03ace1bb9fcebb080bb69218 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 24 Feb 2026 16:32:21 +0800 Subject: [PATCH 2/2] fixup Signed-off-by: tison --- pqb/tests/expr.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pqb/tests/expr.rs b/pqb/tests/expr.rs index 262a1c0..cc9b4be 100644 --- a/pqb/tests/expr.rs +++ b/pqb/tests/expr.rs @@ -83,14 +83,29 @@ fn select_is_distinct_from() { .validate(), @r#"SELECT * FROM "t" WHERE "c1" + 1 IS DISTINCT FROM "c2" + 2"# ); +} + +#[test] +fn select_is_null() { + let c1 = Expr::column("c1"); assert_snapshot!( Select::new() .expr(Expr::asterisk()) .from("t") - .and_where(left.clone().add(Expr::value(1)).is_null()) + .and_where(c1.clone().add(Expr::value(1)).is_null()) .to_sql() .validate(), @r#"SELECT * FROM "t" WHERE "c1" + 1 IS NULL"# ); + + assert_snapshot!( + Select::new() + .expr(Expr::asterisk()) + .from("t") + .and_where(c1.clone().add(Expr::value(1)).is_not_null()) + .to_sql() + .validate(), + @r#"SELECT * FROM "t" WHERE "c1" + 1 IS NOT NULL"# + ); }