mirror of
https://github.com/nushell/nushell.git
synced 2024-12-22 15:13:01 +01:00
Database commands (#5307)
* database commands * db commands * filesystem opens sqlite file * clippy error * corrected error in ci file * removes matrix flag from ci * flax matrix for clippy * add conditional compile for tests * add conditional compile for tests * correct order of command * correct error msg * correct typo
This commit is contained in:
parent
c20ba95885
commit
e94d13da1b
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@ debian/nu/
|
||||
|
||||
# VSCode's IDE items
|
||||
.vscode/*
|
||||
|
||||
# Helix configuration folder
|
||||
.helix
|
||||
|
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -2357,6 +2357,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"sha2 0.10.2",
|
||||
"shadow-rs",
|
||||
"sqlparser",
|
||||
"strip-ansi-escapes",
|
||||
"sysinfo",
|
||||
"terminal_size",
|
||||
@ -4051,6 +4052,15 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparser"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9a527b68048eb95495a1508f6c8395c8defcff5ecdbe8ad4106d08a2ef2a3c"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
|
@ -85,6 +85,9 @@ trash-support = ["nu-command/trash-support"]
|
||||
# Dataframe feature for nushell
|
||||
dataframe = ["nu-command/dataframe"]
|
||||
|
||||
# Database commands for nushell
|
||||
database = ["nu-command/database"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s" # Optimize for size
|
||||
strip = "debuginfo"
|
||||
|
@ -24,7 +24,6 @@ nu-term-grid = { path = "../nu-term-grid", version = "0.61.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.61.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.61.1" }
|
||||
nu-ansi-term = "0.45.1"
|
||||
rusqlite = { version = "0.27.0", features = ["bundled"] }
|
||||
|
||||
# Potential dependencies for extras
|
||||
base64 = "0.13.0"
|
||||
@ -81,6 +80,8 @@ uuid = { version = "0.8.2", features = ["v4"] }
|
||||
which = { version = "4.2.2", optional = true }
|
||||
reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]}
|
||||
wax = { version = "0.4.0", features = ["diagnostics"] }
|
||||
rusqlite = { version = "0.27.0", features = ["bundled"], optional = true }
|
||||
sqlparser = { version = "0.16.0", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
umask = "1.0.0"
|
||||
@ -105,6 +106,7 @@ trash-support = ["trash"]
|
||||
which-support = ["which"]
|
||||
plugin = ["nu-parser/plugin"]
|
||||
dataframe = ["polars", "num"]
|
||||
database = ["sqlparser", "rusqlite"]
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = "0.11.0"
|
||||
|
49
crates/nu-command/src/database/commands/collect.rs
Normal file
49
crates/nu-command/src/database/commands/collect.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature,
|
||||
};
|
||||
|
||||
use super::super::SQLiteDatabase;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollectDb;
|
||||
|
||||
impl Command for CollectDb {
|
||||
fn name(&self) -> &str {
|
||||
"db collect"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Query a database using SQL."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Collect from a select query",
|
||||
example: "open foo.db | db select a | db from table_1 | db collect",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "collect"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
|
||||
|
||||
db.collect(call.head)
|
||||
.map(IntoPipelineData::into_pipeline_data)
|
||||
}
|
||||
}
|
42
crates/nu-command/src/database/commands/command.rs
Normal file
42
crates/nu-command/src/database/commands/command.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use nu_engine::get_full_help;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, IntoPipelineData, PipelineData, ShellError, Signature, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database;
|
||||
|
||||
impl Command for Database {
|
||||
fn name(&self) -> &str {
|
||||
"db"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Database commands"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: get_full_help(
|
||||
&Database.signature(),
|
||||
&Database.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
),
|
||||
span: call.head,
|
||||
}
|
||||
.into_pipeline_data())
|
||||
}
|
||||
}
|
47
crates/nu-command/src/database/commands/describe.rs
Normal file
47
crates/nu-command/src/database/commands/describe.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature,
|
||||
};
|
||||
|
||||
use super::super::SQLiteDatabase;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DescribeDb;
|
||||
|
||||
impl Command for DescribeDb {
|
||||
fn name(&self) -> &str {
|
||||
"db describe"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Describes connection and query of the DB object"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Describe SQLite database constructed query",
|
||||
example: "db open foo.db | db select table_1 | db describe",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "SQLite"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
|
||||
Ok(db.describe(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
130
crates/nu-command/src/database/commands/from.rs
Normal file
130
crates/nu-command/src/database/commands/from.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use super::super::SQLiteDatabase;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape,
|
||||
};
|
||||
use sqlparser::ast::{Ident, ObjectName, Query, Select, SetExpr, TableFactor, TableWithJoins};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FromDb;
|
||||
|
||||
impl Command for FromDb {
|
||||
fn name(&self) -> &str {
|
||||
"db from"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Select section from query statement for a DB"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
"select",
|
||||
SyntaxShape::String,
|
||||
"Name of table to select from",
|
||||
)
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "from"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Selects table from database",
|
||||
example: "db open db.mysql | db from table_a",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let table: String = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
|
||||
db.query = match db.query {
|
||||
None => Some(create_query(table)),
|
||||
Some(query) => Some(modify_query(query, table)),
|
||||
};
|
||||
|
||||
Ok(db.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_query(table: String) -> Query {
|
||||
Query {
|
||||
with: None,
|
||||
body: SetExpr::Select(Box::new(create_select(table))),
|
||||
order_by: Vec::new(),
|
||||
limit: None,
|
||||
offset: None,
|
||||
fetch: None,
|
||||
lock: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn modify_query(mut query: Query, table: String) -> Query {
|
||||
query.body = match query.body {
|
||||
SetExpr::Select(select) => SetExpr::Select(Box::new(modify_select(select, table))),
|
||||
_ => SetExpr::Select(Box::new(create_select(table))),
|
||||
};
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
fn modify_select(select: Box<Select>, table: String) -> Select {
|
||||
Select {
|
||||
from: create_from(table),
|
||||
..select.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_select(table: String) -> Select {
|
||||
Select {
|
||||
distinct: false,
|
||||
top: None,
|
||||
projection: Vec::new(),
|
||||
into: None,
|
||||
from: create_from(table),
|
||||
lateral_views: Vec::new(),
|
||||
selection: None,
|
||||
group_by: Vec::new(),
|
||||
cluster_by: Vec::new(),
|
||||
distribute_by: Vec::new(),
|
||||
sort_by: Vec::new(),
|
||||
having: None,
|
||||
}
|
||||
}
|
||||
|
||||
// This function needs more work
|
||||
// It needs to define multi tables and joins
|
||||
// I assume we will need to define expressions for the columns instead of strings
|
||||
fn create_from(table: String) -> Vec<TableWithJoins> {
|
||||
let ident = Ident {
|
||||
value: table,
|
||||
quote_style: None,
|
||||
};
|
||||
|
||||
let table_factor = TableFactor::Table {
|
||||
name: ObjectName(vec![ident]),
|
||||
alias: None,
|
||||
args: Vec::new(),
|
||||
with_hints: Vec::new(),
|
||||
};
|
||||
|
||||
let table = TableWithJoins {
|
||||
relation: table_factor,
|
||||
joins: Vec::new(),
|
||||
};
|
||||
|
||||
vec![table]
|
||||
}
|
31
crates/nu-command/src/database/commands/mod.rs
Normal file
31
crates/nu-command/src/database/commands/mod.rs
Normal file
@ -0,0 +1,31 @@
|
||||
mod collect;
|
||||
mod command;
|
||||
mod describe;
|
||||
mod from;
|
||||
mod open;
|
||||
mod query;
|
||||
mod select;
|
||||
mod utils;
|
||||
|
||||
use collect::CollectDb;
|
||||
use command::Database;
|
||||
use describe::DescribeDb;
|
||||
use from::FromDb;
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
use open::OpenDb;
|
||||
use query::QueryDb;
|
||||
use select::SelectDb;
|
||||
|
||||
pub fn add_database_decls(working_set: &mut StateWorkingSet) {
|
||||
macro_rules! bind_command {
|
||||
( $command:expr ) => {
|
||||
working_set.add_decl(Box::new($command));
|
||||
};
|
||||
( $( $command:expr ),* ) => {
|
||||
$( working_set.add_decl(Box::new($command)); )*
|
||||
};
|
||||
}
|
||||
|
||||
// Series commands
|
||||
bind_command!(CollectDb, Database, DescribeDb, FromDb, QueryDb, SelectDb, OpenDb);
|
||||
}
|
52
crates/nu-command/src/database/commands/open.rs
Normal file
52
crates/nu-command/src/database/commands/open.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use super::super::SQLiteDatabase;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenDb;
|
||||
|
||||
impl Command for OpenDb {
|
||||
fn name(&self) -> &str {
|
||||
"db open"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required("query", SyntaxShape::Filepath, "SQLite file to be opened")
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Open a database"
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "open"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "",
|
||||
example: r#"""#,
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let path: Spanned<PathBuf> = call.req(engine_state, stack, 0)?;
|
||||
|
||||
SQLiteDatabase::try_from_path(path.item.as_path(), path.span)
|
||||
.map(|db| db.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
57
crates/nu-command/src/database/commands/query.rs
Normal file
57
crates/nu-command/src/database/commands/query.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape,
|
||||
};
|
||||
|
||||
use super::super::SQLiteDatabase;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QueryDb;
|
||||
|
||||
impl Command for QueryDb {
|
||||
fn name(&self) -> &str {
|
||||
"db query"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
"query",
|
||||
SyntaxShape::String,
|
||||
"SQL to execute against the database",
|
||||
)
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Query a database using SQL."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Get 1 table out of a SQLite database",
|
||||
example: r#"db open foo.db | db query "SELECT * FROM Bar""#,
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "SQLite"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let sql: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
|
||||
db.query(&sql, call.head)
|
||||
.map(IntoPipelineData::into_pipeline_data)
|
||||
}
|
||||
}
|
131
crates/nu-command/src/database/commands/select.rs
Normal file
131
crates/nu-command/src/database/commands/select.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use super::{super::SQLiteDatabase, utils::extract_strings};
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
|
||||
};
|
||||
use sqlparser::ast::{Expr, Ident, Query, Select, SelectItem, SetExpr};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SelectDb;
|
||||
|
||||
impl Command for SelectDb {
|
||||
fn name(&self) -> &str {
|
||||
"db select"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Creates a select statement for a DB"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
"select",
|
||||
SyntaxShape::Any,
|
||||
"Select expression(s) on the table",
|
||||
)
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "select"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "selects a column from a database",
|
||||
example: "db open db.mysql | db select a",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "selects columns from a database",
|
||||
example: "db open db.mysql | db select [a, b, c]",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let value: Value = call.req(engine_state, stack, 0)?;
|
||||
let expressions = extract_strings(value)?;
|
||||
|
||||
let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
|
||||
db.query = match db.query {
|
||||
None => Some(create_query(expressions)),
|
||||
Some(query) => Some(modify_query(query, expressions)),
|
||||
};
|
||||
|
||||
Ok(db.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_query(expressions: Vec<String>) -> Query {
|
||||
Query {
|
||||
with: None,
|
||||
body: SetExpr::Select(Box::new(create_select(expressions))),
|
||||
order_by: Vec::new(),
|
||||
limit: None,
|
||||
offset: None,
|
||||
fetch: None,
|
||||
lock: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn modify_query(mut query: Query, expressions: Vec<String>) -> Query {
|
||||
query.body = match query.body {
|
||||
SetExpr::Select(select) => SetExpr::Select(Box::new(modify_select(select, expressions))),
|
||||
_ => SetExpr::Select(Box::new(create_select(expressions))),
|
||||
};
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
fn modify_select(select: Box<Select>, expressions: Vec<String>) -> Select {
|
||||
Select {
|
||||
projection: create_projection(expressions),
|
||||
..select.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_select(expressions: Vec<String>) -> Select {
|
||||
Select {
|
||||
distinct: false,
|
||||
top: None,
|
||||
projection: create_projection(expressions),
|
||||
into: None,
|
||||
from: Vec::new(),
|
||||
lateral_views: Vec::new(),
|
||||
selection: None,
|
||||
group_by: Vec::new(),
|
||||
cluster_by: Vec::new(),
|
||||
distribute_by: Vec::new(),
|
||||
sort_by: Vec::new(),
|
||||
having: None,
|
||||
}
|
||||
}
|
||||
|
||||
// This function needs more work
|
||||
// It needs to define alias and functions in the columns
|
||||
// I assume we will need to define expressions for the columns instead of strings
|
||||
fn create_projection(expressions: Vec<String>) -> Vec<SelectItem> {
|
||||
expressions
|
||||
.into_iter()
|
||||
.map(|expression| {
|
||||
let expr = Expr::Identifier(Ident {
|
||||
value: expression,
|
||||
quote_style: None,
|
||||
});
|
||||
|
||||
SelectItem::UnnamedExpr(expr)
|
||||
})
|
||||
.collect()
|
||||
}
|
15
crates/nu-command/src/database/commands/utils.rs
Normal file
15
crates/nu-command/src/database/commands/utils.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use nu_protocol::{FromValue, ShellError, Value};
|
||||
|
||||
pub fn extract_strings(value: Value) -> Result<Vec<String>, ShellError> {
|
||||
match (
|
||||
<String as FromValue>::from_value(&value),
|
||||
<Vec<String> as FromValue>::from_value(&value),
|
||||
) {
|
||||
(Ok(col), Err(_)) => Ok(vec![col]),
|
||||
(Err(_), Ok(cols)) => Ok(cols),
|
||||
_ => Err(ShellError::IncompatibleParametersSingle(
|
||||
"Expected a string or list of strings".into(),
|
||||
value.span()?,
|
||||
)),
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
mod sqlite;
|
||||
mod commands;
|
||||
mod values;
|
||||
|
||||
pub use sqlite::SQLiteDatabase;
|
||||
pub use commands::add_database_decls;
|
||||
pub(crate) use values::SQLiteDatabase;
|
||||
|
3
crates/nu-command/src/database/values/mod.rs
Normal file
3
crates/nu-command/src/database/values/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod sqlite;
|
||||
|
||||
pub(crate) use sqlite::SQLiteDatabase;
|
@ -1,35 +1,188 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Spanned, Value};
|
||||
use nu_protocol::{CustomValue, PipelineData, ShellError, Span, Spanned, Value};
|
||||
use rusqlite::{types::ValueRef, Connection, Row};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
use sqlparser::ast::Query;
|
||||
|
||||
const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SQLiteDatabase {
|
||||
// I considered storing a SQLite connection here, but decided against it because
|
||||
// 1) YAGNI, 2) it's not obvious how cloning a connection could work, 3) state
|
||||
// management gets tricky quick. Revisit this approach if we find a compelling use case.
|
||||
path: PathBuf,
|
||||
pub query: Option<Query>,
|
||||
}
|
||||
|
||||
// Mocked serialization of the object
|
||||
impl Serialize for SQLiteDatabase {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
// Mocked deserialization of the object
|
||||
impl<'de> Deserialize<'de> for SQLiteDatabase {
|
||||
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = std::path::Path::new("");
|
||||
Ok(SQLiteDatabase::new(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl SQLiteDatabase {
|
||||
pub fn new(path: &Path) -> SQLiteDatabase {
|
||||
SQLiteDatabase {
|
||||
pub fn new(path: &Path) -> Self {
|
||||
Self {
|
||||
path: PathBuf::from(path),
|
||||
query: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_path(path: &Path, span: Span) -> Result<Self, ShellError> {
|
||||
let mut file = File::open(path).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Error opening file".into(),
|
||||
e.to_string(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut buf: [u8; 16] = [0; 16];
|
||||
file.read_exact(&mut buf)
|
||||
.map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Error reading file header".into(),
|
||||
e.to_string(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
.and_then(|_| {
|
||||
if buf == SQLITE_MAGIC_BYTES {
|
||||
Ok(SQLiteDatabase::new(path))
|
||||
} else {
|
||||
Err(ShellError::GenericError(
|
||||
"Error reading file".into(),
|
||||
"Not a SQLite file".into(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query(&self, sql: &Spanned<String>, call_span: Span) -> Result<Value, ShellError> {
|
||||
let db = open_sqlite_db(&self.path, call_span)?;
|
||||
to_shell_error(
|
||||
run_sql_query(db, sql),
|
||||
"Failed to query SQLite database",
|
||||
sql.span,
|
||||
)
|
||||
run_sql_query(db, sql).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Failed to query SQLite database".into(),
|
||||
e.to_string(),
|
||||
Some(sql.span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn describe(&self) -> String {
|
||||
format!("A SQLite database at {:?}", self.path)
|
||||
pub fn collect(&self, call_span: Span) -> Result<Value, ShellError> {
|
||||
let sql = match &self.query {
|
||||
Some(query) => Ok(format!("{}", query)),
|
||||
None => Err(ShellError::GenericError(
|
||||
"Error collecting from db".into(),
|
||||
"No query found in connection".into(),
|
||||
Some(call_span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)),
|
||||
}?;
|
||||
|
||||
let sql = Spanned {
|
||||
item: sql,
|
||||
span: call_span,
|
||||
};
|
||||
|
||||
let db = open_sqlite_db(&self.path, call_span)?;
|
||||
run_sql_query(db, &sql).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Failed to query SQLite database".into(),
|
||||
e.to_string(),
|
||||
Some(sql.span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
|
||||
match value {
|
||||
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
|
||||
Some(db) => Ok(Self {
|
||||
path: db.path.clone(),
|
||||
query: db.query.clone(),
|
||||
}),
|
||||
None => Err(ShellError::CantConvert(
|
||||
"database".into(),
|
||||
"non-database".into(),
|
||||
span,
|
||||
None,
|
||||
)),
|
||||
},
|
||||
x => Err(ShellError::CantConvert(
|
||||
"database".into(),
|
||||
x.get_type().to_string(),
|
||||
x.span()?,
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
|
||||
let value = input.into_value(span);
|
||||
Self::try_from_value(value)
|
||||
}
|
||||
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn describe(&self, span: Span) -> Value {
|
||||
let cols = vec!["connection".to_string(), "query".to_string()];
|
||||
let connection = Value::String {
|
||||
val: self.path.to_str().unwrap_or("").to_string(),
|
||||
span,
|
||||
};
|
||||
|
||||
let query = match &self.query {
|
||||
Some(query) => format!("{query}"),
|
||||
None => "".into(),
|
||||
};
|
||||
|
||||
let query = Value::String { val: query, span };
|
||||
|
||||
Value::Record {
|
||||
cols,
|
||||
vals: vec![connection, query],
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +190,7 @@ impl CustomValue for SQLiteDatabase {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
let cloned = SQLiteDatabase {
|
||||
path: self.path.clone(),
|
||||
query: self.query.clone(),
|
||||
};
|
||||
|
||||
Value::CustomValue {
|
||||
@ -51,11 +205,15 @@ impl CustomValue for SQLiteDatabase {
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
let db = open_sqlite_db(&self.path, span)?;
|
||||
to_shell_error(
|
||||
read_entire_sqlite_db(db, span),
|
||||
"Failed to read from SQLite database",
|
||||
span,
|
||||
)
|
||||
read_entire_sqlite_db(db, span).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Failed to read from SQLite database".into(),
|
||||
e.to_string(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
@ -70,11 +228,15 @@ impl CustomValue for SQLiteDatabase {
|
||||
fn follow_path_string(&self, _column_name: String, span: Span) -> Result<Value, ShellError> {
|
||||
let db = open_sqlite_db(&self.path, span)?;
|
||||
|
||||
to_shell_error(
|
||||
read_single_table(db, _column_name, span),
|
||||
"Failed to read from SQLite database",
|
||||
span,
|
||||
)
|
||||
read_single_table(db, _column_name, span).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Failed to read from SQLite database".into(),
|
||||
e.to_string(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
@ -86,31 +248,52 @@ impl CustomValue for SQLiteDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: is there a more elegant way to map rusqlite errors to ShellErrors?
|
||||
fn to_shell_error<T>(
|
||||
result: Result<T, rusqlite::Error>,
|
||||
message: &str,
|
||||
span: Span,
|
||||
) -> Result<T, nu_protocol::ShellError> {
|
||||
match result {
|
||||
Ok(val) => Ok(val),
|
||||
Err(err) => Err(ShellError::GenericError(
|
||||
message.to_string(),
|
||||
err.to_string(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_sqlite_db(path: &Path, call_span: Span) -> Result<Connection, nu_protocol::ShellError> {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
to_shell_error(
|
||||
Connection::open(path),
|
||||
"Failed to open SQLite database",
|
||||
call_span,
|
||||
)
|
||||
|
||||
Connection::open(path).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Failed to open SQLite database".into(),
|
||||
e.to_string(),
|
||||
Some(call_span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn run_sql_query(conn: Connection, sql: &Spanned<String>) -> Result<Value, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(&sql.item)?;
|
||||
let results = stmt.query([])?;
|
||||
|
||||
let nu_records = results
|
||||
.mapped(|row| Result::Ok(convert_sqlite_row_to_nu_value(row, sql.span)))
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Value>, rusqlite::Error>>()?;
|
||||
|
||||
Ok(Value::List {
|
||||
vals: nu_records,
|
||||
span: sql.span,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_single_table(
|
||||
conn: Connection,
|
||||
table_name: String,
|
||||
call_span: Span,
|
||||
) -> Result<Value, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(&format!("SELECT * FROM {}", table_name))?;
|
||||
let results = stmt.query([])?;
|
||||
|
||||
let nu_records = results
|
||||
.mapped(|row| Result::Ok(convert_sqlite_row_to_nu_value(row, call_span)))
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Value>, rusqlite::Error>>()?;
|
||||
|
||||
Ok(Value::List {
|
||||
vals: nu_records,
|
||||
span: call_span,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_entire_sqlite_db(conn: Connection, call_span: Span) -> Result<Value, rusqlite::Error> {
|
||||
@ -146,41 +329,6 @@ fn read_entire_sqlite_db(conn: Connection, call_span: Span) -> Result<Value, rus
|
||||
span: call_span,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_sql_query(conn: Connection, sql: &Spanned<String>) -> Result<Value, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(&sql.item)?;
|
||||
let mut results = stmt.query([])?;
|
||||
let mut nu_records = Vec::new();
|
||||
|
||||
while let Some(table_row) = results.next()? {
|
||||
nu_records.push(convert_sqlite_row_to_nu_value(table_row, sql.span))
|
||||
}
|
||||
|
||||
Ok(Value::List {
|
||||
vals: nu_records,
|
||||
span: sql.span,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_single_table(
|
||||
conn: Connection,
|
||||
table_name: String,
|
||||
call_span: Span,
|
||||
) -> Result<Value, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(&format!("SELECT * FROM {}", table_name))?;
|
||||
let mut results = stmt.query([])?;
|
||||
let mut nu_records = Vec::new();
|
||||
|
||||
while let Some(table_row) = results.next()? {
|
||||
nu_records.push(convert_sqlite_row_to_nu_value(table_row, call_span))
|
||||
}
|
||||
|
||||
Ok(Value::List {
|
||||
vals: nu_records,
|
||||
span: call_span,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_sqlite_row_to_nu_value(row: &Row, span: Span) -> Value {
|
||||
let mut vals = Vec::new();
|
||||
let colnamestr = row.as_ref().column_names().to_vec();
|
@ -23,6 +23,11 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
#[cfg(feature = "dataframe")]
|
||||
add_dataframe_decls(&mut working_set);
|
||||
|
||||
// Database-related
|
||||
// Adds all related commands to query databases
|
||||
#[cfg(feature = "database")]
|
||||
add_database_decls(&mut working_set);
|
||||
|
||||
// Core
|
||||
bind_command! {
|
||||
Alias,
|
||||
@ -361,11 +366,6 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
ViewSource,
|
||||
};
|
||||
|
||||
// Database-related
|
||||
bind_command! {
|
||||
QueryDb
|
||||
};
|
||||
|
||||
// Deprecated
|
||||
bind_command! {
|
||||
PivotDeprecated,
|
||||
|
@ -1,5 +1,4 @@
|
||||
use crate::filesystem::util::BufferedReader;
|
||||
use crate::SQLiteDatabase;
|
||||
use nu_engine::{eval_block, get_full_help, CallExt};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
@ -7,7 +6,10 @@ use nu_protocol::{
|
||||
Category, Example, IntoPipelineData, PipelineData, RawStream, ShellError, Signature, Spanned,
|
||||
SyntaxShape, Value,
|
||||
};
|
||||
use std::io::{BufReader, Read, Seek};
|
||||
use std::io::BufReader;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
use crate::database::SQLiteDatabase;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@ -44,10 +46,8 @@ impl Command for Open {
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let raw = call.has_flag("raw");
|
||||
|
||||
let call_span = call.head;
|
||||
let ctrlc = engine_state.ctrlc.clone();
|
||||
|
||||
let path = call.opt::<Spanned<String>>(engine_state, stack, 0)?;
|
||||
|
||||
let path = if let Some(path) = path {
|
||||
@ -105,7 +105,17 @@ impl Command for Open {
|
||||
Vec::new(),
|
||||
))
|
||||
} else {
|
||||
let mut file = match std::fs::File::open(path) {
|
||||
#[cfg(feature = "database")]
|
||||
if !raw {
|
||||
let res = SQLiteDatabase::try_from_path(path, arg_span)
|
||||
.map(|db| db.into_value(call.head).into_pipeline_data());
|
||||
|
||||
if res.is_ok() {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
let file = match std::fs::File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
return Err(ShellError::GenericError(
|
||||
@ -118,25 +128,6 @@ impl Command for Open {
|
||||
}
|
||||
};
|
||||
|
||||
// Peek at the file to see if we can detect a SQLite database
|
||||
if !raw {
|
||||
let sqlite_magic_bytes = "SQLite format 3\0".as_bytes();
|
||||
let mut buf: [u8; 16] = [0; 16];
|
||||
|
||||
if file.read_exact(&mut buf).is_ok() && buf == sqlite_magic_bytes {
|
||||
let custom_val = Value::CustomValue {
|
||||
val: Box::new(SQLiteDatabase::new(path)),
|
||||
span: call.head,
|
||||
};
|
||||
|
||||
return Ok(custom_val.into_pipeline_data());
|
||||
}
|
||||
|
||||
if file.rewind().is_err() {
|
||||
return Err(ShellError::IOError("Failed to rewind file".into()));
|
||||
};
|
||||
}
|
||||
|
||||
let buf_reader = BufReader::new(file);
|
||||
|
||||
let output = PipelineData::ExternalStream {
|
||||
|
@ -1,6 +1,5 @@
|
||||
mod conversions;
|
||||
mod core_commands;
|
||||
mod database;
|
||||
mod date;
|
||||
mod default_context;
|
||||
mod deprecated;
|
||||
@ -16,7 +15,6 @@ mod math;
|
||||
mod network;
|
||||
mod path;
|
||||
mod platform;
|
||||
mod query;
|
||||
mod random;
|
||||
mod shells;
|
||||
mod strings;
|
||||
@ -25,7 +23,6 @@ mod viewers;
|
||||
|
||||
pub use conversions::*;
|
||||
pub use core_commands::*;
|
||||
pub use database::*;
|
||||
pub use date::*;
|
||||
pub use default_context::*;
|
||||
pub use deprecated::*;
|
||||
@ -42,7 +39,6 @@ pub use math::*;
|
||||
pub use network::*;
|
||||
pub use path::*;
|
||||
pub use platform::*;
|
||||
pub use query::*;
|
||||
pub use random::*;
|
||||
pub use shells::*;
|
||||
pub use strings::*;
|
||||
@ -54,3 +50,9 @@ mod dataframe;
|
||||
|
||||
#[cfg(feature = "dataframe")]
|
||||
pub use dataframe::*;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
mod database;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
pub use database::*;
|
||||
|
@ -1,96 +0,0 @@
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
use crate::database::SQLiteDatabase;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubCommand;
|
||||
|
||||
impl Command for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"query db"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("query db")
|
||||
.required(
|
||||
"query",
|
||||
SyntaxShape::String,
|
||||
"SQL to execute against the database",
|
||||
)
|
||||
.category(Category::Date) // TODO: change category
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Query a database using SQL."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "SQLite"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let sql: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let call_head = call.head;
|
||||
|
||||
input.map(
|
||||
move |value| query_input(value, call_head, &sql),
|
||||
engine_state.ctrlc.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Get 1 table out of a SQLite database",
|
||||
example: r#"open foo.db | query db "SELECT * FROM Bar""#,
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn query_input(input: Value, call_span: Span, sql: &Spanned<String>) -> Value {
|
||||
// this fn has slightly awkward error handling because it needs to jam errors into Value instead of returning a Result
|
||||
if let Value::CustomValue {
|
||||
val,
|
||||
span: input_span,
|
||||
} = input
|
||||
{
|
||||
let sqlite = val.as_any().downcast_ref::<SQLiteDatabase>();
|
||||
|
||||
if let Some(db) = sqlite {
|
||||
return match db.query(sql, call_span) {
|
||||
Ok(val) => val,
|
||||
Err(error) => Value::Error { error },
|
||||
};
|
||||
}
|
||||
|
||||
return Value::Error {
|
||||
error: ShellError::PipelineMismatch(
|
||||
"a SQLite database".to_string(),
|
||||
call_span,
|
||||
input_span,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
match input.span() {
|
||||
Ok(input_span) => Value::Error {
|
||||
error: ShellError::PipelineMismatch(
|
||||
"a SQLite database".to_string(),
|
||||
call_span,
|
||||
input_span,
|
||||
),
|
||||
},
|
||||
Err(err) => Value::Error { error: err },
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
mod db;
|
||||
|
||||
pub use db::SubCommand as QueryDb;
|
@ -39,6 +39,7 @@ mod open;
|
||||
mod parse;
|
||||
mod path;
|
||||
mod prepend;
|
||||
#[cfg(feature = "database")]
|
||||
mod query;
|
||||
mod random;
|
||||
mod range;
|
||||
|
@ -108,8 +108,8 @@ fn parses_more_bson_complexity() {
|
||||
// │ 4 │ │
|
||||
// ╰───┴──────╯
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[test]
|
||||
|
||||
fn parses_sqlite() {
|
||||
let actual = nu!(
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
@ -123,6 +123,7 @@ fn parses_sqlite() {
|
||||
assert_eq!(actual.out, "3");
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[test]
|
||||
fn parses_sqlite_get_column_name() {
|
||||
let actual = nu!(
|
||||
|
@ -1,13 +1,12 @@
|
||||
use nu_test_support::{nu, pipeline};
|
||||
|
||||
#[test]
|
||||
|
||||
fn can_query_single_table() {
|
||||
let actual = nu!(
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
r#"
|
||||
open sample.db
|
||||
| query db "select * from strings"
|
||||
| db query "select * from strings"
|
||||
| where x =~ ell
|
||||
| length
|
||||
"#
|
||||
@ -22,7 +21,7 @@ fn invalid_sql_fails() {
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
r#"
|
||||
open sample.db
|
||||
| query db "select *asdfasdf"
|
||||
| db query "select *asdfasdf"
|
||||
"#
|
||||
));
|
||||
|
||||
@ -32,11 +31,11 @@ fn invalid_sql_fails() {
|
||||
#[test]
|
||||
fn invalid_input_fails() {
|
||||
let actual = nu!(
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
r#"
|
||||
"foo" | query db "select * from asdf"
|
||||
"foo" | db query "select * from asdf"
|
||||
"#
|
||||
));
|
||||
|
||||
assert!(actual.err.contains("pipeline_mismatch"));
|
||||
assert!(actual.err.contains("can't convert string"));
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ fn where_not_in_table() {
|
||||
assert_eq!(actual.out, "4");
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[test]
|
||||
fn binary_operator_comparisons() {
|
||||
let actual = nu!(
|
||||
@ -109,6 +110,7 @@ fn binary_operator_comparisons() {
|
||||
assert_eq!(actual.out, "42");
|
||||
}
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
#[test]
|
||||
fn contains_operator() {
|
||||
let actual = nu!(
|
||||
|
Loading…
Reference in New Issue
Block a user