Fix headers command handling of missing values (#9603)

# Description
This fixes the `headers` command handling of missing values (issue
#9602). Previously, each row in the table would have its columns set to
be exactly equal to the first row even if it had less columns than the
first row. This would cause to values magically change their column or
cause panics in other commands if rows ended up having more columns than
values.

# Tests
Added a missing values  test for the `headers` command
This commit is contained in:
Ian Manske 2023-07-06 17:54:59 +00:00 committed by GitHub
parent 504eff73f0
commit f38657e6f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 44 additions and 15 deletions

View file

@ -89,28 +89,38 @@ impl Command for Headers {
let config = engine_state.get_config(); let config = engine_state.get_config();
let metadata = input.metadata(); let metadata = input.metadata();
let value = input.into_value(call.head); let value = input.into_value(call.head);
let headers = extract_headers(&value, config)?; let (old_headers, new_headers) = extract_headers(&value, config)?;
let new_headers = replace_headers(value, &headers)?; let new_headers = replace_headers(value, &old_headers, &new_headers)?;
Ok(new_headers.into_pipeline_data().set_metadata(metadata)) Ok(new_headers.into_pipeline_data().set_metadata(metadata))
} }
} }
fn replace_headers(value: Value, headers: &[String]) -> Result<Value, ShellError> { fn replace_headers(
value: Value,
old_headers: &[String],
new_headers: &[String],
) -> Result<Value, ShellError> {
match value { match value {
Value::Record { vals, span, .. } => { Value::Record { cols, vals, span } => {
let vals = vals.into_iter().take(headers.len()).collect(); let (cols, vals) = cols
Ok(Value::Record { .into_iter()
cols: headers.to_owned(), .zip(vals)
vals, .filter_map(|(col, val)| {
span, old_headers
.iter()
.position(|c| c == &col)
.map(|i| (new_headers[i].clone(), val))
}) })
.unzip();
Ok(Value::Record { cols, vals, span })
} }
Value::List { vals, span } => { Value::List { vals, span } => {
let vals = vals let vals = vals
.into_iter() .into_iter()
.skip(1) .skip(1)
.map(|value| replace_headers(value, headers)) .map(|value| replace_headers(value, old_headers, new_headers))
.collect::<Result<Vec<Value>, ShellError>>()?; .collect::<Result<Vec<Value>, ShellError>>()?;
Ok(Value::List { vals, span }) Ok(Value::List { vals, span })
@ -133,9 +143,12 @@ fn is_valid_header(value: &Value) -> bool {
) )
} }
fn extract_headers(value: &Value, config: &Config) -> Result<Vec<String>, ShellError> { fn extract_headers(
value: &Value,
config: &Config,
) -> Result<(Vec<String>, Vec<String>), ShellError> {
match value { match value {
Value::Record { vals, .. } => { Value::Record { cols, vals, .. } => {
for v in vals { for v in vals {
if !is_valid_header(v) { if !is_valid_header(v) {
return Err(ShellError::TypeMismatch { return Err(ShellError::TypeMismatch {
@ -146,7 +159,8 @@ fn extract_headers(value: &Value, config: &Config) -> Result<Vec<String>, ShellE
} }
} }
Ok(vals let old_headers = cols.to_vec();
let new_headers = vals
.iter() .iter()
.enumerate() .enumerate()
.map(|(idx, value)| { .map(|(idx, value)| {
@ -157,7 +171,9 @@ fn extract_headers(value: &Value, config: &Config) -> Result<Vec<String>, ShellE
col col
} }
}) })
.collect::<Vec<String>>()) .collect::<Vec<String>>();
Ok((old_headers, new_headers))
} }
Value::List { vals, span } => vals Value::List { vals, span } => vals
.iter() .iter()

View file

@ -30,6 +30,19 @@ fn headers_adds_missing_column_name() {
assert_eq!(actual.out, r#"["r1c1","r2c1"]"#) assert_eq!(actual.out, r#"["r1c1","r2c1"]"#)
} }
#[test]
fn headers_handles_missing_values() {
let actual = nu!(pipeline(
r#"
[{x: a, y: b}, {x: 1, y: 2}, {x: 1, z: 3}]
| headers
| to nuon --raw
"#
));
assert_eq!(actual.out, "[{a: 1, b: 2}, {a: 1}]")
}
#[test] #[test]
fn headers_invalid_column_type_empty_record() { fn headers_invalid_column_type_empty_record() {
let actual = nu!( let actual = nu!(