forked from extern/nushell
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's IDE items
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
|
# Helix configuration folder
|
||||||
|
.helix
|
||||||
|
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -2357,6 +2357,7 @@ dependencies = [
|
|||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"sha2 0.10.2",
|
"sha2 0.10.2",
|
||||||
"shadow-rs",
|
"shadow-rs",
|
||||||
|
"sqlparser",
|
||||||
"strip-ansi-escapes",
|
"strip-ansi-escapes",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
@ -4051,6 +4052,15 @@ dependencies = [
|
|||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparser"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e9a527b68048eb95495a1508f6c8395c8defcff5ecdbe8ad4106d08a2ef2a3c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -85,6 +85,9 @@ trash-support = ["nu-command/trash-support"]
|
|||||||
# Dataframe feature for nushell
|
# Dataframe feature for nushell
|
||||||
dataframe = ["nu-command/dataframe"]
|
dataframe = ["nu-command/dataframe"]
|
||||||
|
|
||||||
|
# Database commands for nushell
|
||||||
|
database = ["nu-command/database"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s" # Optimize for size
|
opt-level = "s" # Optimize for size
|
||||||
strip = "debuginfo"
|
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-test-support = { path = "../nu-test-support", version = "0.61.1" }
|
||||||
nu-utils = { path = "../nu-utils", version = "0.61.1" }
|
nu-utils = { path = "../nu-utils", version = "0.61.1" }
|
||||||
nu-ansi-term = "0.45.1"
|
nu-ansi-term = "0.45.1"
|
||||||
rusqlite = { version = "0.27.0", features = ["bundled"] }
|
|
||||||
|
|
||||||
# Potential dependencies for extras
|
# Potential dependencies for extras
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
@ -81,6 +80,8 @@ uuid = { version = "0.8.2", features = ["v4"] }
|
|||||||
which = { version = "4.2.2", optional = true }
|
which = { version = "4.2.2", optional = true }
|
||||||
reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]}
|
reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]}
|
||||||
wax = { version = "0.4.0", features = ["diagnostics"] }
|
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]
|
[target.'cfg(unix)'.dependencies]
|
||||||
umask = "1.0.0"
|
umask = "1.0.0"
|
||||||
@ -105,6 +106,7 @@ trash-support = ["trash"]
|
|||||||
which-support = ["which"]
|
which-support = ["which"]
|
||||||
plugin = ["nu-parser/plugin"]
|
plugin = ["nu-parser/plugin"]
|
||||||
dataframe = ["polars", "num"]
|
dataframe = ["polars", "num"]
|
||||||
|
database = ["sqlparser", "rusqlite"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = "0.11.0"
|
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 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 {
|
pub struct SQLiteDatabase {
|
||||||
// I considered storing a SQLite connection here, but decided against it because
|
// 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
|
// 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.
|
// management gets tricky quick. Revisit this approach if we find a compelling use case.
|
||||||
path: PathBuf,
|
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 {
|
impl SQLiteDatabase {
|
||||||
pub fn new(path: &Path) -> SQLiteDatabase {
|
pub fn new(path: &Path) -> Self {
|
||||||
SQLiteDatabase {
|
Self {
|
||||||
path: PathBuf::from(path),
|
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> {
|
pub fn query(&self, sql: &Spanned<String>, call_span: Span) -> Result<Value, ShellError> {
|
||||||
let db = open_sqlite_db(&self.path, call_span)?;
|
let db = open_sqlite_db(&self.path, call_span)?;
|
||||||
to_shell_error(
|
run_sql_query(db, sql).map_err(|e| {
|
||||||
run_sql_query(db, sql),
|
ShellError::GenericError(
|
||||||
"Failed to query SQLite database",
|
"Failed to query SQLite database".into(),
|
||||||
sql.span,
|
e.to_string(),
|
||||||
)
|
Some(sql.span),
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn describe(&self) -> String {
|
pub fn collect(&self, call_span: Span) -> Result<Value, ShellError> {
|
||||||
format!("A SQLite database at {:?}", self.path)
|
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 {
|
fn clone_value(&self, span: Span) -> Value {
|
||||||
let cloned = SQLiteDatabase {
|
let cloned = SQLiteDatabase {
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
|
query: self.query.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Value::CustomValue {
|
Value::CustomValue {
|
||||||
@ -51,11 +205,15 @@ impl CustomValue for SQLiteDatabase {
|
|||||||
|
|
||||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||||
let db = open_sqlite_db(&self.path, span)?;
|
let db = open_sqlite_db(&self.path, span)?;
|
||||||
to_shell_error(
|
read_entire_sqlite_db(db, span).map_err(|e| {
|
||||||
read_entire_sqlite_db(db, span),
|
ShellError::GenericError(
|
||||||
"Failed to read from SQLite database",
|
"Failed to read from SQLite database".into(),
|
||||||
span,
|
e.to_string(),
|
||||||
)
|
Some(span),
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
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> {
|
fn follow_path_string(&self, _column_name: String, span: Span) -> Result<Value, ShellError> {
|
||||||
let db = open_sqlite_db(&self.path, span)?;
|
let db = open_sqlite_db(&self.path, span)?;
|
||||||
|
|
||||||
to_shell_error(
|
read_single_table(db, _column_name, span).map_err(|e| {
|
||||||
read_single_table(db, _column_name, span),
|
ShellError::GenericError(
|
||||||
"Failed to read from SQLite database",
|
"Failed to read from SQLite database".into(),
|
||||||
span,
|
e.to_string(),
|
||||||
)
|
Some(span),
|
||||||
|
None,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn typetag_name(&self) -> &'static str {
|
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> {
|
fn open_sqlite_db(path: &Path, call_span: Span) -> Result<Connection, nu_protocol::ShellError> {
|
||||||
let path = path.to_string_lossy().to_string();
|
let path = path.to_string_lossy().to_string();
|
||||||
to_shell_error(
|
|
||||||
Connection::open(path),
|
Connection::open(path).map_err(|e| {
|
||||||
"Failed to open SQLite database",
|
ShellError::GenericError(
|
||||||
call_span,
|
"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> {
|
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,
|
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 {
|
fn convert_sqlite_row_to_nu_value(row: &Row, span: Span) -> Value {
|
||||||
let mut vals = Vec::new();
|
let mut vals = Vec::new();
|
||||||
let colnamestr = row.as_ref().column_names().to_vec();
|
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")]
|
#[cfg(feature = "dataframe")]
|
||||||
add_dataframe_decls(&mut working_set);
|
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
|
// Core
|
||||||
bind_command! {
|
bind_command! {
|
||||||
Alias,
|
Alias,
|
||||||
@ -361,11 +366,6 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
|||||||
ViewSource,
|
ViewSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database-related
|
|
||||||
bind_command! {
|
|
||||||
QueryDb
|
|
||||||
};
|
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
bind_command! {
|
bind_command! {
|
||||||
PivotDeprecated,
|
PivotDeprecated,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use crate::filesystem::util::BufferedReader;
|
use crate::filesystem::util::BufferedReader;
|
||||||
use crate::SQLiteDatabase;
|
|
||||||
use nu_engine::{eval_block, get_full_help, CallExt};
|
use nu_engine::{eval_block, get_full_help, CallExt};
|
||||||
use nu_protocol::ast::Call;
|
use nu_protocol::ast::Call;
|
||||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||||
@ -7,7 +6,10 @@ use nu_protocol::{
|
|||||||
Category, Example, IntoPipelineData, PipelineData, RawStream, ShellError, Signature, Spanned,
|
Category, Example, IntoPipelineData, PipelineData, RawStream, ShellError, Signature, Spanned,
|
||||||
SyntaxShape, Value,
|
SyntaxShape, Value,
|
||||||
};
|
};
|
||||||
use std::io::{BufReader, Read, Seek};
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
use crate::database::SQLiteDatabase;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
@ -44,10 +46,8 @@ impl Command for Open {
|
|||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||||
let raw = call.has_flag("raw");
|
let raw = call.has_flag("raw");
|
||||||
|
|
||||||
let call_span = call.head;
|
let call_span = call.head;
|
||||||
let ctrlc = engine_state.ctrlc.clone();
|
let ctrlc = engine_state.ctrlc.clone();
|
||||||
|
|
||||||
let path = call.opt::<Spanned<String>>(engine_state, stack, 0)?;
|
let path = call.opt::<Spanned<String>>(engine_state, stack, 0)?;
|
||||||
|
|
||||||
let path = if let Some(path) = path {
|
let path = if let Some(path) = path {
|
||||||
@ -105,7 +105,17 @@ impl Command for Open {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
))
|
))
|
||||||
} else {
|
} 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,
|
Ok(file) => file,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(ShellError::GenericError(
|
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 buf_reader = BufReader::new(file);
|
||||||
|
|
||||||
let output = PipelineData::ExternalStream {
|
let output = PipelineData::ExternalStream {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
mod conversions;
|
mod conversions;
|
||||||
mod core_commands;
|
mod core_commands;
|
||||||
mod database;
|
|
||||||
mod date;
|
mod date;
|
||||||
mod default_context;
|
mod default_context;
|
||||||
mod deprecated;
|
mod deprecated;
|
||||||
@ -16,7 +15,6 @@ mod math;
|
|||||||
mod network;
|
mod network;
|
||||||
mod path;
|
mod path;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod query;
|
|
||||||
mod random;
|
mod random;
|
||||||
mod shells;
|
mod shells;
|
||||||
mod strings;
|
mod strings;
|
||||||
@ -25,7 +23,6 @@ mod viewers;
|
|||||||
|
|
||||||
pub use conversions::*;
|
pub use conversions::*;
|
||||||
pub use core_commands::*;
|
pub use core_commands::*;
|
||||||
pub use database::*;
|
|
||||||
pub use date::*;
|
pub use date::*;
|
||||||
pub use default_context::*;
|
pub use default_context::*;
|
||||||
pub use deprecated::*;
|
pub use deprecated::*;
|
||||||
@ -42,7 +39,6 @@ pub use math::*;
|
|||||||
pub use network::*;
|
pub use network::*;
|
||||||
pub use path::*;
|
pub use path::*;
|
||||||
pub use platform::*;
|
pub use platform::*;
|
||||||
pub use query::*;
|
|
||||||
pub use random::*;
|
pub use random::*;
|
||||||
pub use shells::*;
|
pub use shells::*;
|
||||||
pub use strings::*;
|
pub use strings::*;
|
||||||
@ -54,3 +50,9 @@ mod dataframe;
|
|||||||
|
|
||||||
#[cfg(feature = "dataframe")]
|
#[cfg(feature = "dataframe")]
|
||||||
pub use 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 parse;
|
||||||
mod path;
|
mod path;
|
||||||
mod prepend;
|
mod prepend;
|
||||||
|
#[cfg(feature = "database")]
|
||||||
mod query;
|
mod query;
|
||||||
mod random;
|
mod random;
|
||||||
mod range;
|
mod range;
|
||||||
|
@ -108,8 +108,8 @@ fn parses_more_bson_complexity() {
|
|||||||
// │ 4 │ │
|
// │ 4 │ │
|
||||||
// ╰───┴──────╯
|
// ╰───┴──────╯
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn parses_sqlite() {
|
fn parses_sqlite() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: "tests/fixtures/formats", pipeline(
|
cwd: "tests/fixtures/formats", pipeline(
|
||||||
@ -123,6 +123,7 @@ fn parses_sqlite() {
|
|||||||
assert_eq!(actual.out, "3");
|
assert_eq!(actual.out, "3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_sqlite_get_column_name() {
|
fn parses_sqlite_get_column_name() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
use nu_test_support::{nu, pipeline};
|
use nu_test_support::{nu, pipeline};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn can_query_single_table() {
|
fn can_query_single_table() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: "tests/fixtures/formats", pipeline(
|
cwd: "tests/fixtures/formats", pipeline(
|
||||||
r#"
|
r#"
|
||||||
open sample.db
|
open sample.db
|
||||||
| query db "select * from strings"
|
| db query "select * from strings"
|
||||||
| where x =~ ell
|
| where x =~ ell
|
||||||
| length
|
| length
|
||||||
"#
|
"#
|
||||||
@ -22,7 +21,7 @@ fn invalid_sql_fails() {
|
|||||||
cwd: "tests/fixtures/formats", pipeline(
|
cwd: "tests/fixtures/formats", pipeline(
|
||||||
r#"
|
r#"
|
||||||
open sample.db
|
open sample.db
|
||||||
| query db "select *asdfasdf"
|
| db query "select *asdfasdf"
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -32,11 +31,11 @@ fn invalid_sql_fails() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn invalid_input_fails() {
|
fn invalid_input_fails() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: "tests/fixtures/formats", pipeline(
|
cwd: "tests/fixtures/formats", pipeline(
|
||||||
r#"
|
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");
|
assert_eq!(actual.out, "4");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
#[test]
|
#[test]
|
||||||
fn binary_operator_comparisons() {
|
fn binary_operator_comparisons() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
@ -109,6 +110,7 @@ fn binary_operator_comparisons() {
|
|||||||
assert_eq!(actual.out, "42");
|
assert_eq!(actual.out, "42");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
#[test]
|
#[test]
|
||||||
fn contains_operator() {
|
fn contains_operator() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
|
Loading…
Reference in New Issue
Block a user