diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 521eb5efce..17be5afb29 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -57,6 +57,7 @@ pub fn create_default_context() -> EngineState { GroupBy, Headers, Insert, + Join, SplitBy, Take, Merge, diff --git a/crates/nu-command/src/filters/join.rs b/crates/nu-command/src/filters/join.rs new file mode 100644 index 0000000000..8786ec126d --- /dev/null +++ b/crates/nu-command/src/filters/join.rs @@ -0,0 +1,422 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Config, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, +}; +use std::cmp::max; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone)] +pub struct Join; + +enum JoinType { + Inner, + Left, + Right, + Outer, +} + +enum IncludeInner { + No, + Yes, +} + +type RowEntries<'a> = Vec<(&'a Vec, &'a Vec)>; + +const EMPTY_COL_NAMES: &Vec = &vec![]; + +impl Command for Join { + fn name(&self) -> &str { + "join" + } + + fn signature(&self) -> Signature { + Signature::build("join") + .required( + "right-table", + SyntaxShape::List(Box::new(SyntaxShape::Any)), + "The right table in the join", + ) + .required( + "left-on", + SyntaxShape::String, + "Name of column in input (left) table to join on", + ) + .optional( + "right-on", + SyntaxShape::String, + "Name of column in right table to join on. Defaults to same column as left table.", + ) + .switch("inner", "Inner join (default)", Some('i')) + .switch("left", "Left-outer join", Some('l')) + .switch("right", "Right-outer join", Some('r')) + .switch("outer", "Outer join", Some('o')) + .input_output_types(vec![(Type::Table(vec![]), Type::Table(vec![]))]) + } + + fn usage(&self) -> &str { + "Join two tables" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["sql"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let table_2: Value = call.req(engine_state, stack, 0)?; + let l_on: Value = call.req(engine_state, stack, 1)?; + let r_on: Value = call + .opt(engine_state, stack, 2)? + .unwrap_or_else(|| l_on.clone()); + let span = call.head; + let join_type = join_type(call)?; + + // FIXME: we should handle ListStreams properly instead of collecting + let collected_input = input.into_value(span); + + match (&collected_input, &table_2, &l_on, &r_on) { + ( + Value::List { vals: rows_1, .. }, + Value::List { vals: rows_2, .. }, + Value::String { val: l_on, .. }, + Value::String { val: r_on, .. }, + ) => { + let result = join(rows_1, rows_2, l_on, r_on, join_type, span); + Ok(PipelineData::Value(result, None)) + } + _ => Err(ShellError::UnsupportedInput( + "(PipelineData, table, string, string)".into(), + format!( + "({:?}, {:?}, {:?} {:?})", + collected_input, + table_2.get_type(), + l_on.get_type(), + r_on.get_type(), + ), + span, + span, + )), + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Join two tables", + example: "[{a: 1 b: 2}] | join [{a: 1 c: 3}] a", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["a".into(), "b".into(), "c".into()], + vals: vec![ + Value::Int { + val: 1, + span: Span::test_data(), + }, + Value::Int { + val: 2, + span: Span::test_data(), + }, + Value::Int { + val: 3, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), + }] + } +} + +fn join_type(call: &Call) -> Result { + match ( + call.has_flag("inner"), + call.has_flag("left"), + call.has_flag("right"), + call.has_flag("outer"), + ) { + (_, false, false, false) => Ok(JoinType::Inner), + (false, true, false, false) => Ok(JoinType::Left), + (false, false, true, false) => Ok(JoinType::Right), + (false, false, false, true) => Ok(JoinType::Outer), + _ => Err(ShellError::UnsupportedInput( + "Choose one of: --inner, --left, --right, --outer".into(), + "".into(), + call.head, + call.head, + )), + } +} + +fn join( + left: &Vec, + right: &Vec, + left_join_key: &str, + right_join_key: &str, + join_type: JoinType, + span: Span, +) -> Value { + // Inner / Right Join + // ------------------ + // Make look-up table from rows on left + // For each row r on right: + // If any matching rows on left: + // For each matching row l on left: + // Emit (l, r) + // Else if RightJoin: + // Emit (null, r) + + // Left Join + // ---------- + // Make look-up table from rows on right + // For each row l on left: + // If any matching rows on right: + // For each matching row r on right: + // Emit (l, r) + // Else: + // Emit (l, null) + + // Outer Join + // ---------- + // Perform Left Join procedure + // Perform Right Join procedure, but excluding rows in Inner Join + + let config = Config::default(); + let sep = ","; + let cap = max(left.len(), right.len()); + let shared_join_key = if left_join_key == right_join_key { + Some(left_join_key) + } else { + None + }; + + // For the "other" table, create a map from value in `on` column to a list of the + // rows having that value. + let mut result: Vec = Vec::new(); + let is_outer = matches!(join_type, JoinType::Outer); + let (this, this_join_key, other, other_keys, join_type) = match join_type { + JoinType::Left | JoinType::Outer => ( + left, + left_join_key, + lookup_table(right, right_join_key, sep, cap, &config), + column_names(right), + // For Outer we do a Left pass and a Right pass; this is the Left + // pass. + JoinType::Left, + ), + JoinType::Inner | JoinType::Right => ( + right, + right_join_key, + lookup_table(left, left_join_key, sep, cap, &config), + column_names(left), + join_type, + ), + }; + join_rows( + &mut result, + this, + this_join_key, + other, + other_keys, + shared_join_key, + &join_type, + IncludeInner::Yes, + sep, + &config, + span, + ); + if is_outer { + let (this, this_join_key, other, other_names, join_type) = ( + right, + right_join_key, + lookup_table(left, left_join_key, sep, cap, &config), + column_names(left), + JoinType::Right, + ); + join_rows( + &mut result, + this, + this_join_key, + other, + other_names, + shared_join_key, + &join_type, + IncludeInner::No, + sep, + &config, + span, + ); + } + Value::List { vals: result, span } +} + +// Join rows of `this` (a nushell table) to rows of `other` (a lookup-table +// containing rows of a nushell table). +#[allow(clippy::too_many_arguments)] +fn join_rows( + result: &mut Vec, + this: &Vec, + this_join_key: &str, + other: HashMap, + other_keys: &Vec, + shared_join_key: Option<&str>, + join_type: &JoinType, + include_inner: IncludeInner, + sep: &str, + config: &Config, + span: Span, +) { + for this_row in this { + if let Value::Record { + cols: this_cols, + vals: this_vals, + .. + } = this_row + { + if let Some(this_valkey) = this_row.get_data_by_key(this_join_key) { + if let Some(other_rows) = other.get(&this_valkey.into_string(sep, config)) { + if matches!(include_inner, IncludeInner::Yes) { + for (other_cols, other_vals) in other_rows { + // `other` table contains rows matching `this` row on the join column + let (res_cols, res_vals) = match join_type { + JoinType::Inner | JoinType::Right => merge_records( + (other_cols, other_vals), // `other` (lookup) is the left input table + (this_cols, this_vals), + shared_join_key, + ), + JoinType::Left => merge_records( + (this_cols, this_vals), // `this` is the left input table + (other_cols, other_vals), + shared_join_key, + ), + _ => panic!("not implemented"), + }; + result.push(Value::Record { + cols: res_cols, + vals: res_vals, + span, + }) + } + } + } else if !matches!(join_type, JoinType::Inner) { + // `other` table did not contain any rows matching + // `this` row on the join column; emit a single joined + // row with null values for columns not present, + let other_vals = other_keys + .iter() + .map(|key| { + if Some(key.as_ref()) == shared_join_key { + this_row + .get_data_by_key(key) + .unwrap_or_else(|| Value::nothing(span)) + } else { + Value::nothing(span) + } + }) + .collect(); + let (res_cols, res_vals) = match join_type { + JoinType::Inner | JoinType::Right => merge_records( + (other_keys, &other_vals), + (this_cols, this_vals), + shared_join_key, + ), + JoinType::Left => merge_records( + (this_cols, this_vals), + (other_keys, &other_vals), + shared_join_key, + ), + _ => panic!("not implemented"), + }; + + result.push(Value::Record { + cols: res_cols, + vals: res_vals, + span, + }) + } + } // else { a row is missing a value for the join column } + }; + } +} + +// Return column names (i.e. ordered keys from the first row; we assume that +// these are the same for all rows). +fn column_names(table: &[Value]) -> &Vec { + table + .iter() + .find_map(|val| match val { + Value::Record { cols, .. } => Some(cols), + _ => None, + }) + .unwrap_or(EMPTY_COL_NAMES) +} + +// Create a map from value in `on` column to a list of the rows having that +// value. +fn lookup_table<'a>( + rows: &'a Vec, + on: &str, + sep: &str, + cap: usize, + config: &Config, +) -> HashMap> { + let mut map = HashMap::::with_capacity(cap); + for row in rows { + if let Value::Record { cols, vals, .. } = row { + if let Some(val) = &row.get_data_by_key(on) { + let valkey = val.into_string(sep, config); + map.entry(valkey).or_default().push((cols, vals)); + } + }; + } + map +} + +// Merge `left` and `right` records, renaming keys in `right` where they clash +// with keys in `left`. If `shared_key` is supplied then it is the name of a key +// that should not be renamed (its values are guaranteed to be equal). +fn merge_records( + left: (&Vec, &Vec), + right: (&Vec, &Vec), + shared_key: Option<&str>, +) -> (Vec, Vec) { + let ((l_keys, l_vals), (r_keys, r_vals)) = (left, right); + let cap = max(l_keys.len(), r_keys.len()); + let mut seen = HashSet::with_capacity(cap); + let (mut res_keys, mut res_vals) = (Vec::with_capacity(cap), Vec::with_capacity(cap)); + for (k, v) in l_keys.iter().zip(l_vals) { + res_keys.push(k.clone()); + res_vals.push(v.clone()); + seen.insert(k); + } + + for (k, v) in r_keys.iter().zip(r_vals) { + let k_seen = seen.contains(k); + let k_shared = shared_key == Some(k); + // Do not output shared join key twice + if !(k_seen && k_shared) { + res_keys.push(if k_seen { format!("{}_", k) } else { k.clone() }); + res_vals.push(v.clone()); + } + } + (res_keys, res_vals) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Join {}) + } +} diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 5f6296eb54..00da2428c4 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -20,6 +20,7 @@ mod group; mod group_by; mod headers; mod insert; +mod join; mod last; mod length; mod lines; @@ -76,6 +77,7 @@ pub use group::Group; pub use group_by::GroupBy; pub use headers::Headers; pub use insert::Insert; +pub use join::Join; pub use last::Last; pub use length::Length; pub use lines::Lines; diff --git a/crates/nu-command/tests/commands/join.rs b/crates/nu-command/tests/commands/join.rs new file mode 100644 index 0000000000..4c66994e02 --- /dev/null +++ b/crates/nu-command/tests/commands/join.rs @@ -0,0 +1,372 @@ +use nu_test_support::nu; + +#[test] +fn cases_where_result_is_same_between_join_types_inner() { + do_cases_where_result_is_same_between_join_types("--inner") +} + +#[test] +fn cases_where_result_differs_between_join_types_inner() { + do_cases_where_result_differs_between_join_types("--inner") +} + +#[test] +fn cases_where_result_differs_between_join_types_with_different_join_keys_inner() { + do_cases_where_result_differs_between_join_types_with_different_join_keys("--inner") +} + +#[test] +fn cases_where_result_is_same_between_join_types_left() { + do_cases_where_result_is_same_between_join_types("--left") +} + +#[test] +fn cases_where_result_is_same_between_join_types_outer() { + do_cases_where_result_is_same_between_join_types("--outer") +} + +#[test] +fn cases_where_result_differs_between_join_types_left() { + do_cases_where_result_differs_between_join_types("--left") +} + +#[test] +fn cases_where_result_differs_between_join_types_with_different_join_keys_left() { + do_cases_where_result_differs_between_join_types_with_different_join_keys("--left") +} + +#[test] +fn cases_where_result_is_same_between_join_types_right() { + do_cases_where_result_is_same_between_join_types("--right") +} + +#[test] +fn cases_where_result_differs_between_join_types_right() { + do_cases_where_result_differs_between_join_types("--right") +} + +#[test] +fn cases_where_result_differs_between_join_types_outer() { + do_cases_where_result_differs_between_join_types("--outer") +} + +#[test] +fn cases_where_result_differs_between_join_types_with_different_join_keys_outer() { + do_cases_where_result_differs_between_join_types_with_different_join_keys("--outer") +} + +fn do_cases_where_result_is_same_between_join_types(join_type: &str) { + // .mode column + // .headers on + for ((left, right, on), expected) in [ + (("[]", "[]", "_"), "[]"), + (("[]", "[{a: 1}]", "_"), "[]"), + (("[{a: 1}]", "[]", "_"), "[]"), + (("[{a: 1}]", "[{a: 1}]", "_"), "[]"), + (("[{a: 1}]", "[{a: 1}]", "a"), "[[a]; [1]]"), + (("[{a: 1} {a: 1}]", "[{a: 1}]", "a"), "[[a]; [1], [1]]"), + (("[{a: 1}]", "[{a: 1} {a: 1}]", "a"), "[[a]; [1], [1]]"), + ( + ("[{a: 1} {a: 1}]", "[{a: 1} {a: 1}]", "a"), + "[[a]; [1], [1], [1], [1]]", + ), + (("[{a: 1 b: 1}]", "[{a: 1}]", "a"), "[[a, b]; [1, 1]]"), + (("[{a: 1}]", "[{a: 1 b: 2}]", "a"), "[[a, b]; [1, 2]]"), + ( + // create table l (a, b); + // create table r (a, b); + // insert into l (a, b) values (1, 1); + // insert into r (a, b) values (1, 2); + // select * from l inner join r on l.a = r.a; + ("[{a: 1 b: 1}]", "[{a: 1 b: 2}]", "a"), + "[[a, b, b_]; [1, 1, 2]]", + ), + (("[{a: 1}]", "[{a: 1 b: 1}]", "a"), "[[a, b]; [1, 1]]"), + ] { + let expr = format!("{} | join {} {} {} | to nuon", left, right, join_type, on); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + + // Test again with streaming input (using `each` to convert the input into a ListStream) + let to_list_stream = "each { |i| $i } | "; + let expr = format!( + "{} | {} join {} {} {} | to nuon", + left, to_list_stream, right, join_type, on + ); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + } +} + +fn do_cases_where_result_differs_between_join_types(join_type: &str) { + // .mode column + // .headers on + for ((left, right, on), join_types) in [ + ( + ("[]", "[{a: 1}]", "a"), + [ + ("--inner", "[]"), + ("--left", "[]"), + ("--right", "[[a]; [1]]"), + ("--outer", "[[a]; [1]]"), + ], + ), + ( + ("[{a: 1}]", "[]", "a"), + [ + ("--inner", "[]"), + ("--left", "[[a]; [1]]"), + ("--right", "[]"), + ("--outer", "[[a]; [1]]"), + ], + ), + ( + ("[{a: 2 b: 1}]", "[{a: 1}]", "a"), + [ + ("--inner", "[]"), + ("--left", "[[a, b]; [2, 1]]"), + ("--right", "[[a, b]; [1, null]]"), + ("--outer", "[[a, b]; [2, 1], [1, null]]"), + ], + ), + ( + ("[{a: 1}]", "[{a: 2 b: 1}]", "a"), + [ + ("--inner", "[]"), + ("--left", "[[a, b]; [1, null]]"), + ("--right", "[[a, b]; [2, 1]]"), + ("--outer", "[[a, b]; [1, null], [2, 1]]"), + ], + ), + ( + // create table l (a, b); + // create table r (a, b); + // insert into l (a, b) values (1, 2); + // insert into r (a, b) values (2, 1); + ("[{a: 1 b: 2}]", "[{a: 2 b: 1}]", "a"), + [ + ("--inner", "[]"), + ("--left", "[[a, b, b_]; [1, 2, null]]"), + // select * from l right outer join r on l.a = r.a; + ("--right", "[[a, b, b_]; [2, null, 1]]"), + ("--outer", "[[a, b, b_]; [1, 2, null], [2, null, 1]]"), + ], + ), + ( + ("[{a: 1 b: 2}]", "[{a: 2 b: 1} {a: 1 b: 1}]", "a"), + [ + ("--inner", "[[a, b, b_]; [1, 2, 1]]"), + ("--left", "[[a, b, b_]; [1, 2, 1]]"), + ("--right", "[[a, b, b_]; [2, null, 1], [1, 2, 1]]"), + ("--outer", "[[a, b, b_]; [1, 2, 1], [2, null, 1]]"), + ], + ), + ( + ( + "[{a: 1 b: 1} {a: 2 b: 2} {a: 3 b: 3}]", + "[{a: 1 c: 1} {a: 3 c: 3}]", + "a", + ), + [ + ("--inner", "[[a, b, c]; [1, 1, 1], [3, 3, 3]]"), + ("--left", "[[a, b, c]; [1, 1, 1], [2, 2, null], [3, 3, 3]]"), + ("--right", "[[a, b, c]; [1, 1, 1], [3, 3, 3]]"), + ("--outer", "[[a, b, c]; [1, 1, 1], [2, 2, null], [3, 3, 3]]"), + ], + ), + ( + // create table l (a, c); + // create table r (a, b); + // insert into l (a, c) values (1, 1), (2, 2), (3, 3); + // insert into r (a, b) values (1, 1), (3, 3), (4, 4); + ( + "[{a: 1 c: 1} {a: 2 c: 2} {a: 3 c: 3}]", + "[{a: 1 b: 1} {a: 3 b: 3} {a: 4 b: 4}]", + "a", + ), + [ + ("--inner", "[[a, c, b]; [1, 1, 1], [3, 3, 3]]"), + ("--left", "[[a, c, b]; [1, 1, 1], [2, 2, null], [3, 3, 3]]"), + // select * from l right outer join r on l.a = r.a; + ("--right", "[[a, c, b]; [1, 1, 1], [3, 3, 3], [4, null, 4]]"), + ( + "--outer", + "[[a, c, b]; [1, 1, 1], [2, 2, null], [3, 3, 3], [4, null, 4]]", + ), + ], + ), + ] { + for (join_type_, expected) in join_types { + if join_type_ == join_type { + let expr = format!("{} | join {} {} {} | to nuon", left, right, join_type, on); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + + // Test again with streaming input (using `each` to convert the input into a ListStream) + let to_list_stream = "each { |i| $i } | "; + let expr = format!( + "{} | {} join {} {} {} | to nuon", + left, to_list_stream, right, join_type, on + ); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + } + } + } +} + +fn do_cases_where_result_differs_between_join_types_with_different_join_keys(join_type: &str) { + // .mode column + // .headers on + for ((left, right, left_on, right_on), join_types) in [ + ( + ("[]", "[{z: 1}]", "a", "z"), + [ + ("--inner", "[]"), + ("--left", "[]"), + ("--right", "[[z]; [1]]"), + ("--outer", "[[z]; [1]]"), + ], + ), + ( + ("[{a: 1}]", "[]", "a", "z"), + [ + ("--inner", "[]"), + ("--left", "[[a]; [1]]"), + ("--right", "[]"), + ("--outer", "[[a]; [1]]"), + ], + ), + ( + ("[{a: 2 b: 1}]", "[{z: 1}]", "a", "z"), + [ + ("--inner", "[]"), + ("--left", "[[a, b, z]; [2, 1, null]]"), + ("--right", "[[a, b, z]; [null, null, 1]]"), + ("--outer", "[[a, b, z]; [2, 1, null], [null, null, 1]]"), + ], + ), + ( + ("[{a: 1}]", "[{z: 2 b: 1}]", "a", "z"), + [ + ("--inner", "[]"), + ("--left", "[[a, z, b]; [1, null, null]]"), + ("--right", "[[a, z, b]; [null, 2, 1]]"), + ("--outer", "[[a, z, b]; [1, null, null], [null, 2, 1]]"), + ], + ), + ( + // create table l (a, b); + // create table r (a, b); + // insert into l (a, b) values (1, 2); + // insert into r (a, b) values (2, 1); + ("[{a: 1 b: 2}]", "[{z: 2 b: 1}]", "a", "z"), + [ + ("--inner", "[]"), + ("--left", "[[a, b, z, b_]; [1, 2, null, null]]"), + // select * from l right outer join r on l.a = r.z; + ("--right", "[[a, b, z, b_]; [null, null, 2, 1]]"), + ( + "--outer", + "[[a, b, z, b_]; [1, 2, null, null], [null, null, 2, 1]]", + ), + ], + ), + ( + ("[{a: 1 b: 2}]", "[{z: 2 b: 1} {z: 1 b: 1}]", "a", "z"), + [ + ("--inner", "[[a, b, z, b_]; [1, 2, 1, 1]]"), + ("--left", "[[a, b, z, b_]; [1, 2, 1, 1]]"), + ( + "--right", + "[[a, b, z, b_]; [null, null, 2, 1], [1, 2, 1, 1]]", + ), + ( + "--outer", + "[[a, b, z, b_]; [1, 2, 1, 1], [null, null, 2, 1]]", + ), + ], + ), + ( + ( + "[{a: 1 b: 1} {a: 2 b: 2} {a: 3 b: 3}]", + "[{z: 1 c: 1} {z: 3 c: 3}]", + "a", + "z", + ), + [ + ("--inner", "[[a, b, z, c]; [1, 1, 1, 1], [3, 3, 3, 3]]"), + ( + "--left", + "[[a, b, z, c]; [1, 1, 1, 1], [2, 2, null, null], [3, 3, 3, 3]]", + ), + ("--right", "[[a, b, z, c]; [1, 1, 1, 1], [3, 3, 3, 3]]"), + ( + "--outer", + "[[a, b, z, c]; [1, 1, 1, 1], [2, 2, null, null], [3, 3, 3, 3]]", + ), + ], + ), + ( + // create table l (a, c); + // create table r (a, b); + // insert into l (a, c) values (1, 1), (2, 2), (3, 3); + // insert into r (a, b) values (1, 1), (3, 3), (4, 4); + ( + "[{a: 1 c: 1} {a: 2 c: 2} {a: 3 c: 3}]", + "[{z: 1 b: 1} {z: 3 b: 3} {z: 4 b: 4}]", + "a", + "z", + ), + [ + ("--inner", "[[a, c, z, b]; [1, 1, 1, 1], [3, 3, 3, 3]]"), + ( + "--left", + "[[a, c, z, b]; [1, 1, 1, 1], [2, 2, null, null], [3, 3, 3, 3]]", + ), + // select * from l right outer join r on l.a = r.z; + ( + "--right", + "[[a, c, z, b]; [1, 1, 1, 1], [3, 3, 3, 3], [null, null, 4, 4]]", + ), + ( + "--outer", + "[[a, c, z, b]; [1, 1, 1, 1], [2, 2, null, null], [3, 3, 3, 3], [null, null, 4, 4]]", + ), + ], + ), + ] { + for (join_type_, expected) in join_types { + if join_type_ == join_type { + let expr = format!("{} | join {} {} {} {} | to nuon", left, right, join_type, left_on, right_on); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + + // Test again with streaming input (using `each` to convert the input into a ListStream) + let to_list_stream = "each { |i| $i } | "; + let expr = format!( + "{} | {} join {} {} {} {} | to nuon", + left, to_list_stream, right, join_type, left_on, right_on + ); + let actual = nu!(cwd: ".", expr).out; + assert_eq!(actual, expected); + } + } + } +} + +#[ignore] +#[test] +fn test_alternative_table_syntax() { + let join_type = "--inner"; + for ((left, right, on), expected) in [ + (("[{a: 1}]", "[{a: 1}]", "a"), "[[a]; [1]]"), + (("[{a: 1}]", "[[a]; [1]]", "a"), "[[a]; [1]]"), + (("[[a]; [1]]", "[{a: 1}]", "a"), "[[a]; [1]]"), + (("[[a]; [1]]", "[[a]; [1]]", "a"), "[[a]; [1]]"), + ] { + let expr = format!("{} | join {} {} {} | to nuon", left, right, join_type, on); + let actual = nu!(cwd: ".", &expr).out; + assert_eq!(actual, expected, "Expression was {}", &expr); + } +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 872d2e9f3e..870131934a 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -41,6 +41,7 @@ mod histogram; mod insert; mod into_filesize; mod into_int; +mod join; mod last; mod length; mod let_;