SQL-style join command for Nushell tables (#8424)

This PR adds a command `join` for performing SQL-style joins on Nushell
tables:

```
〉join -h
Join two tables

Usage:
  > join {flags} <right-table> <left-on> (right-on)

Flags:
  -h, --help - Display the help message for this command
  -i, --inner - Inner join (default)
  -l, --left - Left-outer join
  -r, --right - Right-outer join
  -o, --outer - Outer join

Signatures:
  <table> | join list<any>, <string>, <string?> -> <table>

Parameters:
  right-table <list<any>>: The right table in the join
  left-on <string>: Name of column in input (left) table to join on
  (optional) right-on <string>: Name of column in right table to join on. Defaults to same column as left table.

Examples:
  Join two tables
  > [{a: 1 b: 2}] | join [{a: 1 c: 3}] a
  ╭───┬───┬───╮
  │ a │ b │ c │
  ├───┼───┼───┤
  │ 1 │ 2 │ 3 │
  ╰───┴───┴───╯
```

<table>
    <tbody>
        <tr>
<td><img width="400" alt="image"
src="https://user-images.githubusercontent.com/52205/224578744-eb9d133e-2510-4a3d-bd0a-d615f07a06b7.png"></td>
        </tr>
    </tbody>
  </table>


# User-Facing Changes

Adds a new command `join`

# Tests + Formatting

```
cargo test -p nu-command commands::join
```

Don't forget to add tests that cover your changes.

- [x] `cargo fmt --all -- --check` to check standard code formatting
(`cargo fmt --all` applies these changes)
- [x] `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- [x] `cargo test --workspace` to check that all tests pass

# After Submitting

- [ ] If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.

---------

Co-authored-by: Reilly Wood <reilly.wood@icloud.com>
This commit is contained in:
Dan Davison 2023-03-16 23:57:20 +00:00 committed by GitHub
parent 19beafa865
commit 7625aed200
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 798 additions and 0 deletions

View File

@ -57,6 +57,7 @@ pub fn create_default_context() -> EngineState {
GroupBy,
Headers,
Insert,
Join,
SplitBy,
Take,
Merge,

View File

@ -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<String>, &'a Vec<Value>)>;
const EMPTY_COL_NAMES: &Vec<String> = &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<nu_protocol::PipelineData, nu_protocol::ShellError> {
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>, 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<Example> {
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<JoinType, nu_protocol::ShellError> {
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<Value>,
right: &Vec<Value>,
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<Value> = 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<Value>,
this: &Vec<Value>,
this_join_key: &str,
other: HashMap<String, RowEntries>,
other_keys: &Vec<String>,
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<String> {
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<Value>,
on: &str,
sep: &str,
cap: usize,
config: &Config,
) -> HashMap<String, RowEntries<'a>> {
let mut map = HashMap::<String, RowEntries>::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<String>, &Vec<Value>),
right: (&Vec<String>, &Vec<Value>),
shared_key: Option<&str>,
) -> (Vec<String>, Vec<Value>) {
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 {})
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -41,6 +41,7 @@ mod histogram;
mod insert;
mod into_filesize;
mod into_int;
mod join;
mod last;
mod length;
mod let_;