mirror of
https://github.com/nushell/nushell.git
synced 2025-05-31 15:18:23 +02:00
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR makes two changes related to [run-time pipeline input type checking](https://github.com/nushell/nushell/pull/14741): 1. The check which bypasses type checking for commands with only `Type::Nothing` input types has been expanded to work with commands with multiple `Type::Nothing` inputs for different outputs. For example, `ast` has three input/output type pairs, but all of the inputs are `Type::Nothing`: ``` ╭───┬─────────┬────────╮ │ # │ input │ output │ ├───┼─────────┼────────┤ │ 0 │ nothing │ table │ │ 1 │ nothing │ record │ │ 2 │ nothing │ string │ ╰───┴─────────┴────────╯ ``` Before this PR, passing a value (which would otherwise be ignored) to `ast` caused a run-time type error: ``` Error: nu:🐚:only_supports_this_input_type × Input type not supported. ╭─[entry #1:1:6] 1 │ echo 123 | ast -j -f "hi" · ─┬─ ─┬─ · │ ╰── only nothing, nothing, and nothing input data is supported · ╰── input type: int ╰──── ``` After this PR, no error is raised. This doesn't really matter for `ast` (the only other built-in command with a similar input/output type signature is `cal`), but it's more logically consistent. 2. Bypasses input type-checking (parse-time ***and*** run-time) for some (not all, see below) commands which have both a `Type::Nothing` input and some other non-nothing `Type` input. This is accomplished by adding a `Type::Any` input with the same output as the corresponding `Type::Nothing` input/output pair. This is necessary because some commands are intended to operate on an argument with empty pipeline input, or operate on an empty pipeline input with no argument. This causes issues when a value is implicitly passed to one of these commands. I [discovered this issue](https://discord.com/channels/601130461678272522/615962413203718156/1329945784346611712) when working with an example where the `open` command is used in `sort-by` closure: ```nushell ls | sort-by { open -r $in.name | lines | length } ``` Before this PR (but after the run-time input type checking PR), this error is raised: ``` Error: nu:🐚:only_supports_this_input_type × Input type not supported. ╭─[entry #1:1:1] 1 │ ls | sort-by { open -r $in.name | lines | length } · ─┬ ──┬─ · │ ╰── only nothing and string input data is supported · ╰── input type: record<name: string, type: string, size: filesize, modified: date> ╰──── ``` While this error is technically correct, we don't actually want to return an error here since `open` ignores its pipeline input when an argument is passed. This would be a parse-time error as well if the parser was able to infer that the closure input type was a record, but our type inference isn't that robust currently, so this technically incorrect form snuck by type checking until #14741. However, there are some commands with the same kind of type signature where this behavior is actually desirable. This means we can't just bypass type-checking for any command with a `Type::Nothing` input. These commands operate on true `null` values, rather than ignoring their input. For example, `length` returns `0` when passed a `null` value. It's correct, and even desirable, to throw a run-time error when `length` is passed an unexpected type. For example, a string, which should instead be measured with `str length`: ```nushell ["hello" "world"] | sort-by { length } # => Error: nu:🐚:only_supports_this_input_type # => # => × Input type not supported. # => ╭─[entry #32:1:10] # => 1 │ ["hello" "world"] | sort-by { length } # => · ───┬─── ───┬── # => · │ ╰── only list<any>, binary, and nothing input data is supported # => · ╰── input type: string # => ╰──── ``` We need a more robust way for commands to express how they handle the `Type::Nothing` input case. I think a possible solution here is to allow commands to express that they operate on `PipelineData::Empty`, rather than `Value::Nothing`. Then, a command like `open` could have an empty pipeline input type rather than a `Type::Nothing`, and the parse-time and run-time pipeline input type checks know that `open` will safely ignore an incorrectly typed input. That being said, we have a release coming up and the above solution might take a while to implement, so while unfortunate, bypassing input type-checking for these problematic commands serves as a workaround to avoid breaking changes in the release until a more robust solution is implemented. This PR bypasses input type-checking for the following commands: * `load-env`: can take record of envvars as input or argument * `nu-check`: checks input string or filename argument * `open`: can take filename as input or argument * `polars when`: can be used with input, or can be chained with another `polars when` * `stor insert`: data record can be passed as input or argument * `stor update`: data record can be passed as input or argument * `format date`: `--list` ignores input value * `into datetime`: `--list` ignores input value (also added a `Type::Nothing` input which was missing from this command) These commands have a similar input/output signature to the above commands, but are working as intended: * `cd`: The input/output signature was actually incorrect, `cd` always ignores its input. I fixed this in this PR. * `generate` * `get` * `history import` * `interleave` * `into bool` * `length` # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> As a temporary workaround, pipeline input type-checking for the following commands has been bypassed to avoid undesirable run-time input type checking errors which were previously not caught at parse-time: * `open` * `load-env` * `format date` * `into datetime` * `nu-check` * `stor insert` * `stor update` * `polars when` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> CI became green in the time it took me to type the description 😄 # 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. --> N/A
212 lines
7.3 KiB
Rust
212 lines
7.3 KiB
Rust
use crate::database::{values_to_sql, SQLiteDatabase, MEMORY_DB};
|
|
use nu_engine::command_prelude::*;
|
|
use nu_protocol::Signals;
|
|
use rusqlite::params_from_iter;
|
|
|
|
#[derive(Clone)]
|
|
pub struct StorUpdate;
|
|
|
|
impl Command for StorUpdate {
|
|
fn name(&self) -> &str {
|
|
"stor update"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("stor update")
|
|
.input_output_types(vec![
|
|
(Type::Nothing, Type::table()),
|
|
(Type::record(), Type::table()),
|
|
// FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
|
|
// which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
|
|
(Type::Any, Type::table()),
|
|
])
|
|
.required_named(
|
|
"table-name",
|
|
SyntaxShape::String,
|
|
"name of the table you want to insert into",
|
|
Some('t'),
|
|
)
|
|
.named(
|
|
"update-record",
|
|
SyntaxShape::Record(vec![]),
|
|
"a record of column names and column values to update in the specified table",
|
|
Some('u'),
|
|
)
|
|
.named(
|
|
"where-clause",
|
|
SyntaxShape::String,
|
|
"a sql string to use as a where clause without the WHERE keyword",
|
|
Some('w'),
|
|
)
|
|
.allow_variants_without_examples(true)
|
|
.category(Category::Database)
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Update information in a specified table in the in-memory sqlite database."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["sqlite", "storing", "table", "saving", "changing"]
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
vec![
|
|
Example {
|
|
description: "Update the in-memory sqlite database",
|
|
example: "stor update --table-name nudb --update-record {str1: nushell datetime1: 2020-04-17}",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Update the in-memory sqlite database with a where clause",
|
|
example: "stor update --table-name nudb --update-record {str1: nushell datetime1: 2020-04-17} --where-clause \"bool1 = 1\"",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description: "Update the in-memory sqlite database through pipeline input",
|
|
example: "{str1: nushell datetime1: 2020-04-17} | stor update --table-name nudb",
|
|
result: None,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let span = call.head;
|
|
let table_name: Option<String> = call.get_flag(engine_state, stack, "table-name")?;
|
|
let update_record: Option<Record> = call.get_flag(engine_state, stack, "update-record")?;
|
|
let where_clause_opt: Option<Spanned<String>> =
|
|
call.get_flag(engine_state, stack, "where-clause")?;
|
|
|
|
// Open the in-mem database
|
|
let db = Box::new(SQLiteDatabase::new(
|
|
std::path::Path::new(MEMORY_DB),
|
|
Signals::empty(),
|
|
));
|
|
|
|
// Check if the record is being passed as input or using the update record parameter
|
|
let columns = handle(span, update_record, input)?;
|
|
|
|
process(table_name, span, &db, columns, where_clause_opt)?;
|
|
|
|
Ok(Value::custom(db, span).into_pipeline_data())
|
|
}
|
|
}
|
|
|
|
fn handle(
|
|
span: Span,
|
|
update_record: Option<Record>,
|
|
input: PipelineData,
|
|
) -> Result<Record, ShellError> {
|
|
match input {
|
|
PipelineData::Empty => update_record.ok_or_else(|| ShellError::MissingParameter {
|
|
param_name: "requires a record".into(),
|
|
span,
|
|
}),
|
|
PipelineData::Value(value, ..) => {
|
|
// Since input is being used, check if the data record parameter is used too
|
|
if update_record.is_some() {
|
|
return Err(ShellError::GenericError {
|
|
error: "Pipeline and Flag both being used".into(),
|
|
msg: "Use either pipeline input or '--update-record' parameter".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
match value {
|
|
Value::Record { val, .. } => Ok(val.into_owned()),
|
|
val => Err(ShellError::OnlySupportsThisInputType {
|
|
exp_input_type: "record".into(),
|
|
wrong_type: val.get_type().to_string(),
|
|
dst_span: Span::unknown(),
|
|
src_span: val.span(),
|
|
}),
|
|
}
|
|
}
|
|
_ => {
|
|
if update_record.is_some() {
|
|
return Err(ShellError::GenericError {
|
|
error: "Pipeline and Flag both being used".into(),
|
|
msg: "Use either pipeline input or '--update-record' parameter".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
Err(ShellError::OnlySupportsThisInputType {
|
|
exp_input_type: "record".into(),
|
|
wrong_type: "".into(),
|
|
dst_span: span,
|
|
src_span: span,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process(
|
|
table_name: Option<String>,
|
|
span: Span,
|
|
db: &SQLiteDatabase,
|
|
record: Record,
|
|
where_clause_opt: Option<Spanned<String>>,
|
|
) -> Result<(), ShellError> {
|
|
if table_name.is_none() {
|
|
return Err(ShellError::MissingParameter {
|
|
param_name: "requires at table name".into(),
|
|
span,
|
|
});
|
|
}
|
|
let new_table_name = table_name.unwrap_or("table".into());
|
|
if let Ok(conn) = db.open_connection() {
|
|
let mut update_stmt = format!("UPDATE {} ", new_table_name);
|
|
|
|
update_stmt.push_str("SET ");
|
|
let mut placeholders: Vec<String> = Vec::new();
|
|
|
|
for (index, (key, _)) in record.iter().enumerate() {
|
|
placeholders.push(format!("{} = ?{}", key, index + 1));
|
|
}
|
|
update_stmt.push_str(&placeholders.join(", "));
|
|
|
|
// Yup, this is a bit janky, but I'm not sure a better way to do this without having
|
|
// --and and --or flags as well as supporting ==, !=, <>, is null, is not null, etc.
|
|
// and other sql syntax. So, for now, just type a sql where clause as a string.
|
|
if let Some(where_clause) = where_clause_opt {
|
|
update_stmt.push_str(&format!(" WHERE {}", where_clause.item));
|
|
}
|
|
// dbg!(&update_stmt);
|
|
|
|
// Get the params from the passed values
|
|
let params = values_to_sql(record.values().cloned())?;
|
|
|
|
conn.execute(&update_stmt, params_from_iter(params))
|
|
.map_err(|err| ShellError::GenericError {
|
|
error: "Failed to open SQLite connection in memory from update".into(),
|
|
msg: err.to_string(),
|
|
span: Some(Span::test_data()),
|
|
help: None,
|
|
inner: vec![],
|
|
})?;
|
|
}
|
|
// dbg!(db.clone());
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_examples() {
|
|
use crate::test_examples;
|
|
|
|
test_examples(StorUpdate {})
|
|
}
|
|
}
|