mirror of
https://github.com/nushell/nushell
synced 2024-12-26 13:03:07 +00:00
Change comparison operators to allow nulls (#8617)
Prior to this PR, the less/greater than operators (`<`, `>`, `<=`, `>=`) would throw an error if either side was null. After this PR, these operators return null if either side (or both) is null. ### Examples ```bash 1 < 3 # true 1 < null # null null < 3 # null null < null # null ``` ### Motivation JT [asked the C# folks](https://discord.com/channels/601130461678272522/615329862395101194/1086137515053957140) and this is apparently the approach they would choose for comparison operators if they could start from scratch. This PR makes `where` more convenient to use on jagged/missing data. For example, we can now filter on columns that may not be present in every row: ``` > [{foo: 123} {}] | where foo? > 10 ╭───┬─────╮ │ # │ foo │ ├───┼─────┤ │ 0 │ 123 │ ╰───┴─────╯ ```
This commit is contained in:
parent
0567407f85
commit
d409171ba8
4 changed files with 94 additions and 0 deletions
|
@ -194,3 +194,10 @@ fn fail_on_non_iterator() {
|
|||
|
||||
assert!(actual.err.contains("only_supports_this_input_type"));
|
||||
}
|
||||
|
||||
// Test that filtering on columns that might be missing/null works
|
||||
#[test]
|
||||
fn where_gt_null() {
|
||||
let actual = nu!("[{foo: 123} {}] | where foo? > 10 | to nuon");
|
||||
assert_eq!(actual.out, "[[foo]; [123]]");
|
||||
}
|
||||
|
|
|
@ -295,6 +295,9 @@ pub fn math_result_type(
|
|||
(Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.to_string()), None),
|
||||
(Type::Custom(a), _) => (Type::Custom(a.to_string()), None),
|
||||
|
||||
(Type::Nothing, _) => (Type::Nothing, None),
|
||||
(_, Type::Nothing) => (Type::Nothing, None),
|
||||
|
||||
(Type::Any, _) => (Type::Bool, None),
|
||||
(_, Type::Any) => (Type::Bool, None),
|
||||
_ => {
|
||||
|
@ -322,6 +325,9 @@ pub fn math_result_type(
|
|||
(Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.to_string()), None),
|
||||
(Type::Custom(a), _) => (Type::Custom(a.to_string()), None),
|
||||
|
||||
(Type::Nothing, _) => (Type::Nothing, None),
|
||||
(_, Type::Nothing) => (Type::Nothing, None),
|
||||
|
||||
(Type::Any, _) => (Type::Bool, None),
|
||||
(_, Type::Any) => (Type::Bool, None),
|
||||
_ => {
|
||||
|
@ -351,6 +357,9 @@ pub fn math_result_type(
|
|||
|
||||
(Type::Any, _) => (Type::Bool, None),
|
||||
(_, Type::Any) => (Type::Bool, None),
|
||||
|
||||
(Type::Nothing, _) => (Type::Nothing, None),
|
||||
(_, Type::Nothing) => (Type::Nothing, None),
|
||||
_ => {
|
||||
*op = Expression::garbage(op.span);
|
||||
(
|
||||
|
@ -378,6 +387,9 @@ pub fn math_result_type(
|
|||
|
||||
(Type::Any, _) => (Type::Bool, None),
|
||||
(_, Type::Any) => (Type::Bool, None),
|
||||
|
||||
(Type::Nothing, _) => (Type::Nothing, None),
|
||||
(_, Type::Nothing) => (Type::Nothing, None),
|
||||
_ => {
|
||||
*op = Expression::garbage(op.span);
|
||||
(
|
||||
|
|
|
@ -2658,6 +2658,10 @@ impl Value {
|
|||
return lhs.operation(*span, Operator::Comparison(Comparison::LessThan), op, rhs);
|
||||
}
|
||||
|
||||
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
|
||||
return Ok(Value::nothing(span));
|
||||
}
|
||||
|
||||
if !type_compatible(self.get_type(), rhs.get_type())
|
||||
&& (self.get_type() != Type::Any)
|
||||
&& (rhs.get_type() != Type::Any)
|
||||
|
@ -2697,6 +2701,10 @@ impl Value {
|
|||
);
|
||||
}
|
||||
|
||||
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
|
||||
return Ok(Value::nothing(span));
|
||||
}
|
||||
|
||||
if !type_compatible(self.get_type(), rhs.get_type())
|
||||
&& (self.get_type() != Type::Any)
|
||||
&& (rhs.get_type() != Type::Any)
|
||||
|
@ -2734,6 +2742,10 @@ impl Value {
|
|||
);
|
||||
}
|
||||
|
||||
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
|
||||
return Ok(Value::nothing(span));
|
||||
}
|
||||
|
||||
if !type_compatible(self.get_type(), rhs.get_type())
|
||||
&& (self.get_type() != Type::Any)
|
||||
&& (rhs.get_type() != Type::Any)
|
||||
|
@ -2771,6 +2783,10 @@ impl Value {
|
|||
);
|
||||
}
|
||||
|
||||
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
|
||||
return Ok(Value::nothing(span));
|
||||
}
|
||||
|
||||
if !type_compatible(self.get_type(), rhs.get_type())
|
||||
&& (self.get_type() != Type::Any)
|
||||
&& (rhs.get_type() != Type::Any)
|
||||
|
|
|
@ -119,3 +119,62 @@ fn precedence_of_or_groups() -> TestResult {
|
|||
fn test_filesize_op() -> TestResult {
|
||||
run_test("-5kb + 4.5kb", "-500 B")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lt() -> TestResult {
|
||||
run_test("1 < 3", "true").unwrap();
|
||||
run_test("3 < 3", "false").unwrap();
|
||||
run_test("3 < 1", "false")
|
||||
}
|
||||
|
||||
// Comparison operators return null if 1 side or both side is null.
|
||||
// The motivation for this behaviour: JT asked the C# devs and they said this is
|
||||
// the behaviour they would choose if they were starting from scratch.
|
||||
#[test]
|
||||
fn lt_null() -> TestResult {
|
||||
run_test("3 < null | to nuon", "null").unwrap();
|
||||
run_test("null < 3 | to nuon", "null").unwrap();
|
||||
run_test("null < null | to nuon", "null")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lte() -> TestResult {
|
||||
run_test("1 <= 3", "true").unwrap();
|
||||
run_test("3 <= 3", "true").unwrap();
|
||||
run_test("3 <= 1", "false")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lte_null() -> TestResult {
|
||||
run_test("3 <= null | to nuon", "null").unwrap();
|
||||
run_test("null <= 3 | to nuon", "null").unwrap();
|
||||
run_test("null <= null | to nuon", "null")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt() -> TestResult {
|
||||
run_test("1 > 3", "false").unwrap();
|
||||
run_test("3 > 3", "false").unwrap();
|
||||
run_test("3 > 1", "true")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gt_null() -> TestResult {
|
||||
run_test("3 > null | to nuon", "null").unwrap();
|
||||
run_test("null > 3 | to nuon", "null").unwrap();
|
||||
run_test("null > null | to nuon", "null")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gte() -> TestResult {
|
||||
run_test("1 >= 3", "false").unwrap();
|
||||
run_test("3 >= 3", "true").unwrap();
|
||||
run_test("3 >= 1", "true")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gte_null() -> TestResult {
|
||||
run_test("3 >= null | to nuon", "null").unwrap();
|
||||
run_test("null >= 3 | to nuon", "null").unwrap();
|
||||
run_test("null >= null | to nuon", "null")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue