nushell/crates/nu-command/src/charting/hashable_value.rs
Reilly Wood 21b84a6d65
Optional members in cell paths: Attempt 2 (#8379)
This is a follow up from https://github.com/nushell/nushell/pull/7540.
Please provide feedback if you have the time!

## Summary

This PR lets you use `?` to indicate that a member in a cell path is
optional and Nushell should return `null` if that member cannot be
accessed.

Unlike the previous PR, `?` is now a _postfix_ modifier for cell path
members. A cell path of `.foo?.bar` means that `foo` is optional and
`bar` is not.

`?` does _not_ suppress all errors; it is intended to help in situations
where data has "holes", i.e. the data types are correct but something is
missing. Type mismatches (like trying to do a string path access on a
date) will still fail.

### Record Examples

```bash

{ foo: 123 }.foo # returns 123

{ foo: 123 }.bar # errors
{ foo: 123 }.bar? # returns null

{ foo: 123 } | get bar # errors
{ foo: 123 } | get bar? # returns null

{ foo: 123 }.bar.baz # errors
{ foo: 123 }.bar?.baz # errors because `baz` is not present on the result from `bar?`
{ foo: 123 }.bar.baz? # errors
{ foo: 123 }.bar?.baz? # returns null
```

### List Examples
```
〉[{foo: 1} {foo: 2} {}].foo
Error: nu:🐚:column_not_found

  × Cannot find column
   ╭─[entry #30:1:1]
 1 │ [{foo: 1} {foo: 2} {}].foo
   ·                    ─┬  ─┬─
   ·                     │   ╰── cannot find column 'foo'
   ·                     ╰── value originates here
   ╰────
〉[{foo: 1} {foo: 2} {}].foo?
╭───┬───╮
│ 0 │ 1 │
│ 1 │ 2 │
│ 2 │   │
╰───┴───╯
〉[{foo: 1} {foo: 2} {}].foo?.2 | describe
nothing

〉[a b c].4? | describe
nothing

〉[{foo: 1} {foo: 2} {}] | where foo? == 1
╭───┬─────╮
│ # │ foo │
├───┼─────┤
│ 0 │   1 │
╰───┴─────╯
```

# Breaking changes

1. Column names with `?` in them now need to be quoted.
2. The `-i`/`--ignore-errors` flag has been removed from `get` and
`select`
1. After this PR, most `get` error handling can be done with `?` and/or
`try`/`catch`.
4. Cell path accesses like this no longer work without a `?`:
```bash
〉[{a:1 b:2} {a:3}].b.0
2
```
We had some clever code that was able to recognize that since we only
want row `0`, it's OK if other rows are missing column `b`. I removed
that because it's tricky to maintain, and now that query needs to be
written like:


```bash
〉[{a:1 b:2} {a:3}].b?.0
2
```

I think the regression is acceptable for now. I plan to do more work in
the future to enable streaming of cell path accesses, and when that
happens I'll be able to make `.b.0` work again.
2023-03-15 20:50:58 -07:00

320 lines
10 KiB
Rust

use chrono::{DateTime, FixedOffset};
use nu_protocol::{ShellError, Span, Value};
use std::hash::{Hash, Hasher};
/// A subset of [Value](crate::Value), which is hashable.
/// And it means that we can put the value into something like [HashMap](std::collections::HashMap) or [HashSet](std::collections::HashSet)
/// for further usage like value statistics.
///
/// For now the main way to crate a [HashableValue] is using [from_value](HashableValue::from_value)
///
/// Please note that although each variant contains `span` field, but during hashing, this field will not be concerned.
/// Which means that the following will be true:
/// ```text
/// assert_eq!(HashableValue::Bool {val: true, span: Span{start: 0, end: 1}}, HashableValue::Bool {val: true, span: Span{start: 90, end: 1000}})
/// ```
#[derive(Eq, Debug, Ord, PartialOrd)]
pub enum HashableValue {
Bool {
val: bool,
span: Span,
},
Int {
val: i64,
span: Span,
},
Float {
val: [u8; 8], // because f64 is not hashable, we save it as [u8;8] array to make it hashable.
span: Span,
},
Filesize {
val: i64,
span: Span,
},
Duration {
val: i64,
span: Span,
},
Date {
val: DateTime<FixedOffset>,
span: Span,
},
String {
val: String,
span: Span,
},
Binary {
val: Vec<u8>,
span: Span,
},
}
impl Default for HashableValue {
fn default() -> Self {
HashableValue::Bool {
val: false,
span: Span::unknown(),
}
}
}
impl HashableValue {
/// Try to convert from `value` to self
///
/// A `span` is required because when there is an error in value, it may not contain `span` field.
///
/// If the given value is not hashable(mainly because of it is structured data), an error will returned.
pub fn from_value(value: Value, span: Span) -> Result<Self, ShellError> {
match value {
Value::Bool { val, span } => Ok(HashableValue::Bool { val, span }),
Value::Int { val, span } => Ok(HashableValue::Int { val, span }),
Value::Filesize { val, span } => Ok(HashableValue::Filesize { val, span }),
Value::Duration { val, span } => Ok(HashableValue::Duration { val, span }),
Value::Date { val, span } => Ok(HashableValue::Date { val, span }),
Value::Float { val, span } => Ok(HashableValue::Float {
val: val.to_ne_bytes(),
span,
}),
Value::String { val, span } => Ok(HashableValue::String { val, span }),
Value::Binary { val, span } => Ok(HashableValue::Binary { val, span }),
// Explicitly propagate errors instead of dropping them.
Value::Error { error } => Err(*error),
_ => Err(ShellError::UnsupportedInput(
"input value is not hashable".into(),
format!("input type: {:?}", value.get_type()),
span,
value.expect_span(),
)),
}
}
/// Convert from self to nu's core data type `Value`.
pub fn into_value(self) -> Value {
match self {
HashableValue::Bool { val, span } => Value::Bool { val, span },
HashableValue::Int { val, span } => Value::Int { val, span },
HashableValue::Filesize { val, span } => Value::Filesize { val, span },
HashableValue::Duration { val, span } => Value::Duration { val, span },
HashableValue::Date { val, span } => Value::Date { val, span },
HashableValue::Float { val, span } => Value::Float {
val: f64::from_ne_bytes(val),
span,
},
HashableValue::String { val, span } => Value::String { val, span },
HashableValue::Binary { val, span } => Value::Binary { val, span },
}
}
}
impl Hash for HashableValue {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
HashableValue::Bool { val, .. } => val.hash(state),
HashableValue::Int { val, .. } => val.hash(state),
HashableValue::Filesize { val, .. } => val.hash(state),
HashableValue::Duration { val, .. } => val.hash(state),
HashableValue::Date { val, .. } => val.hash(state),
HashableValue::Float { val, .. } => val.hash(state),
HashableValue::String { val, .. } => val.hash(state),
HashableValue::Binary { val, .. } => val.hash(state),
}
}
}
impl PartialEq for HashableValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(HashableValue::Bool { val: lhs, .. }, HashableValue::Bool { val: rhs, .. }) => {
lhs == rhs
}
(HashableValue::Int { val: lhs, .. }, HashableValue::Int { val: rhs, .. }) => {
lhs == rhs
}
(
HashableValue::Filesize { val: lhs, .. },
HashableValue::Filesize { val: rhs, .. },
) => lhs == rhs,
(
HashableValue::Duration { val: lhs, .. },
HashableValue::Duration { val: rhs, .. },
) => lhs == rhs,
(HashableValue::Date { val: lhs, .. }, HashableValue::Date { val: rhs, .. }) => {
lhs == rhs
}
(HashableValue::Float { val: lhs, .. }, HashableValue::Float { val: rhs, .. }) => {
lhs == rhs
}
(HashableValue::String { val: lhs, .. }, HashableValue::String { val: rhs, .. }) => {
lhs == rhs
}
(HashableValue::Binary { val: lhs, .. }, HashableValue::Binary { val: rhs, .. }) => {
lhs == rhs
}
_ => false,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use nu_protocol::ast::{CellPath, PathMember};
use std::collections::{HashMap, HashSet};
#[test]
fn from_value() {
let span = Span::test_data();
let values = vec![
(
Value::Bool { val: true, span },
HashableValue::Bool { val: true, span },
),
(
Value::Int { val: 1, span },
HashableValue::Int { val: 1, span },
),
(
Value::Filesize { val: 1, span },
HashableValue::Filesize { val: 1, span },
),
(
Value::Duration { val: 1, span },
HashableValue::Duration { val: 1, span },
),
(
Value::Date {
val: DateTime::<FixedOffset>::parse_from_rfc2822(
"Wed, 18 Feb 2015 23:16:09 GMT",
)
.unwrap(),
span,
},
HashableValue::Date {
val: DateTime::<FixedOffset>::parse_from_rfc2822(
"Wed, 18 Feb 2015 23:16:09 GMT",
)
.unwrap(),
span,
},
),
(
Value::String {
val: "1".to_string(),
span,
},
HashableValue::String {
val: "1".to_string(),
span,
},
),
(
Value::Binary { val: vec![1], span },
HashableValue::Binary { val: vec![1], span },
),
];
for (val, expect_hashable_val) in values.into_iter() {
assert_eq!(
HashableValue::from_value(val, Span::unknown()).unwrap(),
expect_hashable_val
);
}
}
#[test]
fn from_unhashable_value() {
let span = Span::test_data();
let values = [
Value::List {
vals: vec![Value::Bool { val: true, span }],
span,
},
Value::Closure {
val: 0,
captures: HashMap::new(),
span,
},
Value::Nothing { span },
Value::Error {
error: Box::new(ShellError::DidYouMean("what?".to_string(), span)),
},
Value::CellPath {
val: CellPath {
members: vec![PathMember::Int {
val: 0,
span,
optional: false,
}],
},
span,
},
];
for v in values {
assert!(HashableValue::from_value(v, Span::unknown()).is_err())
}
}
#[test]
fn from_to_tobe_same() {
let span = Span::test_data();
let values = vec![
Value::Bool { val: true, span },
Value::Int { val: 1, span },
Value::Filesize { val: 1, span },
Value::Duration { val: 1, span },
Value::String {
val: "1".to_string(),
span,
},
Value::Binary { val: vec![1], span },
];
for val in values.into_iter() {
let expected_val = val.clone();
assert_eq!(
HashableValue::from_value(val, Span::unknown())
.unwrap()
.into_value(),
expected_val
);
}
}
#[test]
fn hashable_value_eq_without_concern_span() {
assert_eq!(
HashableValue::Bool {
val: true,
span: Span::new(0, 1)
},
HashableValue::Bool {
val: true,
span: Span::new(90, 1000)
}
)
}
#[test]
fn put_to_hashset() {
let span = Span::test_data();
let mut set = HashSet::new();
set.insert(HashableValue::Bool { val: true, span });
assert!(set.contains(&HashableValue::Bool { val: true, span }));
// hashable value doesn't care about span.
let diff_span = Span::new(1, 2);
set.insert(HashableValue::Bool {
val: true,
span: diff_span,
});
assert!(set.contains(&HashableValue::Bool { val: true, span }));
assert!(set.contains(&HashableValue::Bool {
val: true,
span: diff_span
}));
assert_eq!(set.len(), 1);
set.insert(HashableValue::Int { val: 2, span });
assert_eq!(set.len(), 2);
}
}