Add insert and update back (#4864)

This commit is contained in:
JT 2022-03-18 06:55:02 +13:00 committed by GitHub
parent 6e69d40bb9
commit 0986eefb64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 624 additions and 56 deletions

View file

@ -76,6 +76,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Group,
GroupBy,
Headers,
Insert,
SplitBy,
Keep,
Merge,
@ -107,6 +108,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Transpose,
Uniq,
Upsert,
Update,
UpdateCells,
Where,
Window,
@ -356,7 +358,6 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
// Deprecated
bind_command! {
InsertDeprecated,
PivotDeprecated,
StrDatetimeDeprecated,
StrDecimalDeprecated,

View file

@ -1,36 +0,0 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, PipelineData, Signature,
};
#[derive(Clone)]
pub struct InsertDeprecated;
impl Command for InsertDeprecated {
fn name(&self) -> &str {
"insert"
}
fn signature(&self) -> Signature {
Signature::build(self.name()).category(Category::Deprecated)
}
fn usage(&self) -> &str {
"Deprecated command"
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
Err(nu_protocol::ShellError::DeprecatedCommand(
self.name().to_string(),
"update".to_string(),
call.head,
))
}
}

View file

@ -1,4 +1,3 @@
mod insert;
mod match_;
mod nth;
mod pivot;
@ -7,7 +6,6 @@ mod str_decimal;
mod str_int;
mod unalias;
pub use insert::InsertDeprecated;
pub use match_::MatchDeprecated;
pub use nth::NthDeprecated;
pub use pivot::PivotDeprecated;

View file

@ -0,0 +1,149 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::{Call, CellPath};
use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack};
use nu_protocol::{
Category, Example, FromValue, IntoPipelineData, PipelineData, ShellError, Signature, Span,
SyntaxShape, Value,
};
#[derive(Clone)]
pub struct Insert;
impl Command for Insert {
fn name(&self) -> &str {
"insert"
}
fn signature(&self) -> Signature {
Signature::build("insert")
.required(
"field",
SyntaxShape::CellPath,
"the name of the column to insert",
)
.required(
"new value",
SyntaxShape::Any,
"the new value to give the cell(s)",
)
.category(Category::Filters)
}
fn usage(&self) -> &str {
"Insert a new column."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
insert(engine_state, stack, call, input)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Insert a new value",
example: "echo {'name': 'nu', 'stars': 5} | insert alias 'Nushell'",
result: Some(Value::Record {
cols: vec!["name".into(), "stars".into(), "alias".into()],
vals: vec![
Value::test_string("nu"),
Value::test_int(5),
Value::test_string("Nushell"),
],
span: Span::test_data(),
}),
}]
}
}
fn insert(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
let replacement: Value = call.req(engine_state, stack, 1)?;
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let engine_state = engine_state.clone();
let ctrlc = engine_state.ctrlc.clone();
// Replace is a block, so set it up and run it instead of using it as the replacement
if replacement.as_block().is_ok() {
let capture_block: CaptureBlock = FromValue::from_value(&replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(&capture_block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
input.map(
move |mut input| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone())
}
}
let output = eval_block(
&engine_state,
&mut stack,
&block,
input.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
);
match output {
Ok(pd) => {
if let Err(e) =
input.insert_data_at_cell_path(&cell_path.members, pd.into_value(span))
{
return Value::Error { error: e };
}
input
}
Err(e) => Value::Error { error: e },
}
},
ctrlc,
)
} else {
input.map(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) = input.insert_data_at_cell_path(&cell_path.members, replacement) {
return Value::Error { error: e };
}
input
},
ctrlc,
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(Insert {})
}
}

View file

@ -16,6 +16,7 @@ mod get;
mod group;
mod group_by;
mod headers;
mod insert;
mod keep;
mod last;
mod length;
@ -38,6 +39,7 @@ mod sort_by;
mod split_by;
mod transpose;
mod uniq;
mod update;
mod update_cells;
mod upsert;
mod where_;
@ -63,6 +65,7 @@ pub use get::Get;
pub use group::Group;
pub use group_by::GroupBy;
pub use headers::Headers;
pub use insert::Insert;
pub use keep::*;
pub use last::Last;
pub use length::Length;
@ -85,6 +88,7 @@ pub use sort_by::SortBy;
pub use split_by::SplitBy;
pub use transpose::Transpose;
pub use uniq::*;
pub use update::Update;
pub use update_cells::UpdateCells;
pub use upsert::Upsert;
pub use where_::Where;

View file

@ -0,0 +1,159 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::{Call, CellPath};
use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack};
use nu_protocol::{
Category, Example, FromValue, IntoPipelineData, PipelineData, ShellError, Signature, Span,
SyntaxShape, Value,
};
#[derive(Clone)]
pub struct Update;
impl Command for Update {
fn name(&self) -> &str {
"update"
}
fn signature(&self) -> Signature {
Signature::build("update")
.required(
"field",
SyntaxShape::CellPath,
"the name of the column to update",
)
.required(
"replacement value",
SyntaxShape::Any,
"the new value to give the cell(s)",
)
.category(Category::Filters)
}
fn usage(&self) -> &str {
"Update an existing column to have a new value."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
upsert(engine_state, stack, call, input)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Update a column value",
example: "echo {'name': 'nu', 'stars': 5} | update name 'Nushell'",
result: Some(Value::Record {
cols: vec!["name".into(), "stars".into()],
vals: vec![Value::test_string("Nushell"), Value::test_int(5)],
span: Span::test_data(),
}),
},
Example {
description: "Use in block form for more involved updating logic",
example: "echo [[count fruit]; [1 'apple']] | update count {|f| $f.count + 1}",
result: Some(Value::List {
vals: vec![Value::Record {
cols: vec!["count".into(), "fruit".into()],
vals: vec![Value::test_int(2), Value::test_string("apple")],
span: Span::test_data(),
}],
span: Span::test_data(),
}),
},
]
}
}
fn upsert(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
let replacement: Value = call.req(engine_state, stack, 1)?;
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let engine_state = engine_state.clone();
let ctrlc = engine_state.ctrlc.clone();
// Replace is a block, so set it up and run it instead of using it as the replacement
if replacement.as_block().is_ok() {
let capture_block: CaptureBlock = FromValue::from_value(&replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(&capture_block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
input.map(
move |mut input| {
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone())
}
}
let output = eval_block(
&engine_state,
&mut stack,
&block,
input.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
);
match output {
Ok(pd) => {
if let Err(e) =
input.update_data_at_cell_path(&cell_path.members, pd.into_value(span))
{
return Value::Error { error: e };
}
input
}
Err(e) => Value::Error { error: e },
}
},
ctrlc,
)
} else {
input.map(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) = input.update_data_at_cell_path(&cell_path.members, replacement) {
return Value::Error { error: e };
}
input
},
ctrlc,
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(Update {})
}
}

View file

@ -112,7 +112,7 @@ fn upsert(
match output {
Ok(pd) => {
if let Err(e) =
input.replace_data_at_cell_path(&cell_path.members, pd.into_value(span))
input.upsert_data_at_cell_path(&cell_path.members, pd.into_value(span))
{
return Value::Error { error: e };
}
@ -129,7 +129,7 @@ fn upsert(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) = input.replace_data_at_cell_path(&cell_path.members, replacement) {
if let Err(e) = input.upsert_data_at_cell_path(&cell_path.members, replacement) {
return Value::Error { error: e };
}

View file

@ -0,0 +1,28 @@
use nu_test_support::{nu, pipeline};
#[test]
fn insert_the_column() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| insert dev-dependencies.new_assertions "0.7.0"
| get dev-dependencies.new_assertions
"#
));
assert_eq!(actual.out, "0.7.0");
}
#[test]
fn insert_the_column_conflict() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| insert dev-dependencies.pretty_assertions "0.7.0"
"#
));
assert!(actual.err.contains("column already exists"));
}

View file

@ -23,6 +23,7 @@ mod hash_;
mod headers;
mod help;
mod histogram;
mod insert;
mod into_filesize;
mod into_int;
mod keep;
@ -61,6 +62,7 @@ mod str_;
mod touch;
mod uniq;
mod update;
mod upsert;
mod use_;
mod where_;
#[cfg(feature = "which")]

View file

@ -6,7 +6,7 @@ fn sets_the_column() {
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| upsert dev-dependencies.pretty_assertions "0.7.0"
| update dev-dependencies.pretty_assertions "0.7.0"
| get dev-dependencies.pretty_assertions
"#
));
@ -21,7 +21,7 @@ fn sets_the_column_from_a_block_run_output() {
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| upsert dev-dependencies.pretty_assertions { open cargo_sample.toml | get dev-dependencies.pretty_assertions | inc --minor }
| update dev-dependencies.pretty_assertions { open cargo_sample.toml | get dev-dependencies.pretty_assertions | inc --minor }
| get dev-dependencies.pretty_assertions
"#
));
@ -35,7 +35,7 @@ fn sets_the_column_from_a_block_full_stream_output() {
cwd: "tests/fixtures/formats", pipeline(
r#"
wrap content
| upsert content { open --raw cargo_sample.toml | lines | first 5 }
| update content { open --raw cargo_sample.toml | lines | first 5 }
| get content.1
| str contains "nu"
"#
@ -50,7 +50,7 @@ fn sets_the_column_from_a_subexpression() {
cwd: "tests/fixtures/formats", pipeline(
r#"
wrap content
| upsert content (open --raw cargo_sample.toml | lines | first 5)
| update content (open --raw cargo_sample.toml | lines | first 5)
| get content.1
| str contains "nu"
"#
@ -58,3 +58,16 @@ fn sets_the_column_from_a_subexpression() {
assert_eq!(actual.out, "true");
}
#[test]
fn upsert_column_missing() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| update dev-dependencies.new_assertions "0.7.0"
"#
));
assert!(actual.err.contains("cannot find column"));
}

View file

@ -0,0 +1,60 @@
use nu_test_support::{nu, pipeline};
#[test]
fn sets_the_column() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| upsert dev-dependencies.pretty_assertions "0.7.0"
| get dev-dependencies.pretty_assertions
"#
));
assert_eq!(actual.out, "0.7.0");
}
#[cfg(features = "inc")]
#[test]
fn sets_the_column_from_a_block_run_output() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
open cargo_sample.toml
| upsert dev-dependencies.pretty_assertions { open cargo_sample.toml | get dev-dependencies.pretty_assertions | inc --minor }
| get dev-dependencies.pretty_assertions
"#
));
assert_eq!(actual.out, "0.7.0");
}
#[test]
fn sets_the_column_from_a_block_full_stream_output() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
wrap content
| upsert content { open --raw cargo_sample.toml | lines | first 5 }
| get content.1
| str contains "nu"
"#
));
assert_eq!(actual.out, "true");
}
#[test]
fn sets_the_column_from_a_subexpression() {
let actual = nu!(
cwd: "tests/fixtures/formats", pipeline(
r#"
wrap content
| upsert content (open --raw cargo_sample.toml | lines | first 5)
| get content.1
| str contains "nu"
"#
));
assert_eq!(actual.out, "true");
}

View file

@ -226,7 +226,7 @@ impl PipelineData {
}
}
pub fn update_cell_path(
pub fn upsert_cell_path(
&mut self,
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value>,
@ -238,8 +238,8 @@ impl PipelineData {
vals: stream.collect(),
span: head,
}
.update_cell_path(cell_path, callback),
PipelineData::Value(v, ..) => v.update_cell_path(cell_path, callback),
.upsert_cell_path(cell_path, callback),
PipelineData::Value(v, ..) => v.upsert_cell_path(cell_path, callback),
_ => Ok(()),
}
}

View file

@ -150,6 +150,13 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
#[label = "value originates here"] Span,
),
#[error("Column already exists")]
#[diagnostic(code(nu::shell::column_already_exists), url(docsrs))]
ColumnAlreadyExists(
#[label = "column already exists"] Span,
#[label = "value originates here"] Span,
),
#[error("Not a list value")]
#[diagnostic(code(nu::shell::not_a_list), url(docsrs))]
NotAList(

View file

@ -682,7 +682,7 @@ impl Value {
}
/// Follow a given column path into the value: for example accessing select elements in a stream or list
pub fn update_cell_path(
pub fn upsert_cell_path(
&mut self,
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value>,
@ -693,11 +693,11 @@ impl Value {
match new_val {
Value::Error { error } => Err(error),
new_val => self.replace_data_at_cell_path(cell_path, new_val),
new_val => self.upsert_data_at_cell_path(cell_path, new_val),
}
}
pub fn replace_data_at_cell_path(
pub fn upsert_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
@ -716,7 +716,7 @@ impl Value {
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
found = true;
col.1.replace_data_at_cell_path(
col.1.upsert_data_at_cell_path(
&cell_path[1..],
new_val.clone(),
)?
@ -733,7 +733,7 @@ impl Value {
vals: vec![],
span: new_val.span()?,
};
new_col.replace_data_at_cell_path(
new_col.upsert_data_at_cell_path(
&cell_path[1..],
new_val,
)?;
@ -754,7 +754,7 @@ impl Value {
found = true;
col.1
.replace_data_at_cell_path(&cell_path[1..], new_val.clone())?
.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
}
}
if !found {
@ -767,7 +767,7 @@ impl Value {
vals: vec![],
span: new_val.span()?,
};
new_col.replace_data_at_cell_path(&cell_path[1..], new_val)?;
new_col.upsert_data_at_cell_path(&cell_path[1..], new_val)?;
vals.push(new_col);
}
}
@ -777,7 +777,190 @@ impl Value {
PathMember::Int { val: row_num, span } => match self {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.replace_data_at_cell_path(&cell_path[1..], new_val)?
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
} else {
return Err(ShellError::AccessBeyondEnd(vals.len(), *span));
}
}
v => return Err(ShellError::NotAList(*span, v.span()?)),
},
},
None => {
*self = new_val;
}
}
Ok(())
}
/// Follow a given column path into the value: for example accessing select elements in a stream or list
pub fn update_cell_path(
&mut self,
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value>,
) -> Result<(), ShellError> {
let orig = self.clone();
let new_val = callback(&orig.follow_cell_path(cell_path)?);
match new_val {
Value::Error { error } => Err(error),
new_val => self.update_data_at_cell_path(cell_path, new_val),
}
}
pub fn update_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
) -> Result<(), ShellError> {
match cell_path.first() {
Some(path_member) => match path_member {
PathMember::String {
val: col_name,
span,
} => match self {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
found = true;
col.1.update_data_at_cell_path(
&cell_path[1..],
new_val.clone(),
)?
}
}
if !found {
return Err(ShellError::CantFindColumn(*span, *v_span));
}
}
v => return Err(ShellError::CantFindColumn(*span, v.span()?)),
}
}
}
Value::Record {
cols,
vals,
span: v_span,
} => {
let mut found = false;
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
found = true;
col.1
.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
}
}
if !found {
return Err(ShellError::CantFindColumn(*span, *v_span));
}
}
v => return Err(ShellError::CantFindColumn(*span, v.span()?)),
},
PathMember::Int { val: row_num, span } => match self {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.update_data_at_cell_path(&cell_path[1..], new_val)?
} else {
return Err(ShellError::AccessBeyondEnd(vals.len(), *span));
}
}
v => return Err(ShellError::NotAList(*span, v.span()?)),
},
},
None => {
*self = new_val;
}
}
Ok(())
}
pub fn insert_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
) -> Result<(), ShellError> {
match cell_path.first() {
Some(path_member) => match path_member {
PathMember::String {
val: col_name,
span,
} => match self {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
match val {
Value::Record {
cols,
vals,
span: v_span,
} => {
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
if cell_path.len() == 1 {
return Err(ShellError::ColumnAlreadyExists(
*span, *v_span,
));
} else {
return col.1.insert_data_at_cell_path(
&cell_path[1..],
new_val,
);
}
}
}
cols.push(col_name.clone());
vals.push(new_val.clone());
}
_ => {
return Err(ShellError::UnsupportedInput(
"table or record".into(),
*span,
))
}
}
}
}
Value::Record {
cols,
vals,
span: v_span,
} => {
for col in cols.iter().zip(vals.iter_mut()) {
if col.0 == col_name {
if cell_path.len() == 1 {
return Err(ShellError::ColumnAlreadyExists(*span, *v_span));
} else {
return col
.1
.insert_data_at_cell_path(&cell_path[1..], new_val);
}
}
}
cols.push(col_name.clone());
vals.push(new_val);
}
_ => {
return Err(ShellError::UnsupportedInput(
"table or record".into(),
*span,
))
}
},
PathMember::Int { val: row_num, span } => match self {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.insert_data_at_cell_path(&cell_path[1..], new_val)?
} else {
return Err(ShellError::AccessBeyondEnd(vals.len(), *span));
}