mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 14:40:06 +02:00
Initial --params implementation (#12249)
# Description This PR adds a `--params` param to `query db`. This closes #11643. You can't combine both named and positional parameters, I think this might be a limitation with rusqlite itself. I tried using named parameters with indices like `{ ':named': 123, '1': "positional" }` but that always failed with a rusqlite error. On the flip side, the other way around works: for something like `VALUES (:named, ?)`, you can treat both as positional: `-p [hello 123]`. This PR introduces some very gnarly code repetition in `prepared_statement_to_nu_list`. I tried, I swear; the compiler wasn't having any of it, it kept telling me to box my closures and then it said that the reference lifetimes were incompatible in the match arms. I gave up and put the mapping code in the match itself, but I'm still not happy. Another thing I'm unhappy about: I don't like how you have to put the `:colon` in named parameters. I think nushell should insert it if it's [missing](https://www.sqlite.org/lang_expr.html#parameters). But this is the way [rusqlite works](https://docs.rs/rusqlite/latest/rusqlite/trait.Params.html#example-named), so for now, I'll let it be consistent. Just know that it's not really a blocker, and it isn't a compatibility change to later make `{ colon: 123 }` work, without the quotes and `:`. This would require allocating and turning our pretty little `&str` into a `String`, though # User-Facing Changes Less incentive to leave yourself open to SQL injection with statements like `query db $"INSERT INTO x VALUES \($unsafe_user_input)"`. Additionally, the `$""` syntax being annoying with parentheses plays in our favor, making users even more likely to use ? with `--params`. # Tests + Formatting Hehe
This commit is contained in:
@ -1 +1,2 @@
|
||||
mod into_sqlite;
|
||||
mod query_db;
|
||||
|
115
crates/nu-command/tests/commands/database/query_db.rs
Normal file
115
crates/nu-command/tests/commands/database/query_db.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use nu_test_support::{nu, nu_repl_code, playground::Playground};
|
||||
|
||||
// Multiple nu! calls don't persist state, so we can't store it in a function
|
||||
const DATABASE_INIT: &str = r#"stor open | query db "CREATE TABLE IF NOT EXISTS test_db (
|
||||
name TEXT,
|
||||
age INTEGER,
|
||||
height REAL,
|
||||
serious BOOLEAN,
|
||||
created_at DATETIME,
|
||||
largest_file INTEGER,
|
||||
time_slept INTEGER,
|
||||
null_field TEXT,
|
||||
data BLOB
|
||||
)""#;
|
||||
|
||||
#[test]
|
||||
fn data_types() {
|
||||
Playground::setup("empty", |_, _| {
|
||||
let results = nu!(nu_repl_code(&[
|
||||
DATABASE_INIT,
|
||||
// Add row with our data types
|
||||
r#"stor open
|
||||
| query db "INSERT INTO test_db VALUES (
|
||||
'nimurod',
|
||||
20,
|
||||
6.0,
|
||||
true,
|
||||
date('2024-03-23T00:15:24-03:00'),
|
||||
72400000,
|
||||
1000000,
|
||||
NULL,
|
||||
x'68656c6c6f'
|
||||
)"
|
||||
"#,
|
||||
// Query our table with the row we just added to get its nushell types
|
||||
r#"
|
||||
stor open | query db "SELECT * FROM test_db" | first | values | each { describe } | str join "-"
|
||||
"#
|
||||
]));
|
||||
|
||||
// Assert data types match. Booleans are mapped to "numeric" due to internal SQLite representations:
|
||||
// https://www.sqlite.org/datatype3.html
|
||||
// They are simply 1 or 0 in practice, but the column could contain any valid SQLite value
|
||||
assert_eq!(
|
||||
results.out,
|
||||
"string-int-float-int-string-int-int-nothing-binary"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordered_params() {
|
||||
Playground::setup("empty", |_, _| {
|
||||
let results = nu!(nu_repl_code(&[
|
||||
DATABASE_INIT,
|
||||
// Add row with our data types
|
||||
r#"(stor open
|
||||
| query db "INSERT INTO test_db VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
-p [ 'nimurod', 20, 6.0, true, ('2024-03-23T00:15:24-03:00' | into datetime), 72.4mb, 1ms, null, ("hello" | into binary) ]
|
||||
)"#,
|
||||
// Query our nu values and types
|
||||
r#"
|
||||
let values = (stor open | query db "SELECT * FROM test_db" | first | values);
|
||||
|
||||
($values | str join '-') + "_" + ($values | each { describe } | str join '-')
|
||||
"#
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
results.out,
|
||||
"nimurod-20-6-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\
|
||||
string-int-float-int-string-int-int-nothing-binary"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_params() {
|
||||
Playground::setup("empty", |_, _| {
|
||||
let results = nu!(nu_repl_code(&[
|
||||
DATABASE_INIT,
|
||||
// Add row with our data types. query db should support all possible named parameters
|
||||
// @-prefixed, $-prefixed, and :-prefixed
|
||||
// But :prefix is the "blessed" way to do it, and as such, the only one that's
|
||||
// promoted to from a bare word `key: value` property in the record
|
||||
// In practice, users should not use @param or $param
|
||||
r#"(stor open
|
||||
| query db "INSERT INTO test_db VALUES (:name, :age, @height, $serious, :created_at, :largest_file, :time_slept, :null_field, :data)"
|
||||
-p {
|
||||
name: 'nimurod',
|
||||
':age': 20,
|
||||
'@height': 6.0,
|
||||
'$serious': true,
|
||||
created_at: ('2024-03-23T00:15:24-03:00' | into datetime),
|
||||
largest_file: 72.4mb,
|
||||
time_slept: 1ms,
|
||||
null_field: null,
|
||||
data: ("hello" | into binary)
|
||||
}
|
||||
)"#,
|
||||
// Query our nu values and types
|
||||
r#"
|
||||
let values = (stor open | query db "SELECT * FROM test_db" | first | values);
|
||||
|
||||
($values | str join '-') + "_" + ($values | each { describe } | str join '-')
|
||||
"#
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
results.out,
|
||||
"nimurod-20-6-1-2024-03-23 00:15:24-03:00-72400000-1000000--[104, 101, 108, 108, 111]_\
|
||||
string-int-float-int-string-int-int-nothing-binary"
|
||||
);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user