forked from extern/nushell
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:
parent
19beafa865
commit
7625aed200
@ -57,6 +57,7 @@ pub fn create_default_context() -> EngineState {
|
||||
GroupBy,
|
||||
Headers,
|
||||
Insert,
|
||||
Join,
|
||||
SplitBy,
|
||||
Take,
|
||||
Merge,
|
||||
|
422
crates/nu-command/src/filters/join.rs
Normal file
422
crates/nu-command/src/filters/join.rs
Normal 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 {})
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
372
crates/nu-command/tests/commands/join.rs
Normal file
372
crates/nu-command/tests/commands/join.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ mod histogram;
|
||||
mod insert;
|
||||
mod into_filesize;
|
||||
mod into_int;
|
||||
mod join;
|
||||
mod last;
|
||||
mod length;
|
||||
mod let_;
|
||||
|
Loading…
Reference in New Issue
Block a user