mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 16:33:37 +01:00
Database commands (#5343)
* dabase access commands * select expression * select using expressions * cargo fmt
This commit is contained in:
parent
cd5199de31
commit
5c9fe85ec4
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4102,6 +4102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9a527b68048eb95495a1508f6c8395c8defcff5ecdbe8ad4106d08a2ef2a3c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -81,7 +81,7 @@ 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 }
|
||||
sqlparser = { version = "0.16.0", features = ["serde"], optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
umask = "1.0.0"
|
||||
|
@ -6,19 +6,23 @@ mod open;
|
||||
mod query;
|
||||
mod schema;
|
||||
mod select;
|
||||
mod utils;
|
||||
|
||||
// Temporal module to create Query objects
|
||||
mod testing;
|
||||
use testing::TestingDb;
|
||||
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
|
||||
use collect::CollectDb;
|
||||
use command::Database;
|
||||
use describe::DescribeDb;
|
||||
use from::FromDb;
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
use open::OpenDb;
|
||||
use query::QueryDb;
|
||||
use schema::SchemaDb;
|
||||
use select::SelectDb;
|
||||
use select::ProjectionDb;
|
||||
|
||||
pub fn add_database_decls(working_set: &mut StateWorkingSet) {
|
||||
pub fn add_commands_decls(working_set: &mut StateWorkingSet) {
|
||||
macro_rules! bind_command {
|
||||
( $command:expr ) => {
|
||||
working_set.add_decl(Box::new($command));
|
||||
@ -29,5 +33,15 @@ pub fn add_database_decls(working_set: &mut StateWorkingSet) {
|
||||
}
|
||||
|
||||
// Series commands
|
||||
bind_command!(CollectDb, Database, DescribeDb, FromDb, QueryDb, SelectDb, OpenDb, SchemaDb);
|
||||
bind_command!(
|
||||
CollectDb,
|
||||
Database,
|
||||
DescribeDb,
|
||||
FromDb,
|
||||
QueryDb,
|
||||
ProjectionDb,
|
||||
OpenDb,
|
||||
SchemaDb,
|
||||
TestingDb
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::super::SQLiteDatabase;
|
||||
use crate::database::values::db_row::DbRow;
|
||||
use crate::database::values::definitions::db_row::DbRow;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
|
@ -1,16 +1,16 @@
|
||||
use super::{super::SQLiteDatabase, utils::extract_strings};
|
||||
use super::{super::values::dsl::SelectDb, super::SQLiteDatabase};
|
||||
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};
|
||||
use sqlparser::ast::{Query, Select, SelectItem, SetExpr};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SelectDb;
|
||||
pub struct ProjectionDb;
|
||||
|
||||
impl Command for SelectDb {
|
||||
impl Command for ProjectionDb {
|
||||
fn name(&self) -> &str {
|
||||
"db select"
|
||||
}
|
||||
@ -21,7 +21,7 @@ impl Command for SelectDb {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
.rest(
|
||||
"select",
|
||||
SyntaxShape::Any,
|
||||
"Select expression(s) on the table",
|
||||
@ -42,7 +42,7 @@ impl Command for SelectDb {
|
||||
},
|
||||
Example {
|
||||
description: "selects columns from a database",
|
||||
example: "db open db.mysql | db select [a, b, c]",
|
||||
example: "db open db.mysql | db select a b c",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
@ -55,20 +55,24 @@ impl Command for SelectDb {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let value: Value = call.req(engine_state, stack, 0)?;
|
||||
let expressions = extract_strings(value)?;
|
||||
let vals: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
||||
let value = Value::List {
|
||||
vals,
|
||||
span: call.head,
|
||||
};
|
||||
let projection = SelectDb::extract_selects(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)),
|
||||
None => Some(create_query(projection)),
|
||||
Some(query) => Some(modify_query(query, projection)),
|
||||
};
|
||||
|
||||
Ok(db.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_query(expressions: Vec<String>) -> Query {
|
||||
fn create_query(expressions: Vec<SelectItem>) -> Query {
|
||||
Query {
|
||||
with: None,
|
||||
body: SetExpr::Select(Box::new(create_select(expressions))),
|
||||
@ -80,7 +84,7 @@ fn create_query(expressions: Vec<String>) -> Query {
|
||||
}
|
||||
}
|
||||
|
||||
fn modify_query(mut query: Query, expressions: Vec<String>) -> Query {
|
||||
fn modify_query(mut query: Query, expressions: Vec<SelectItem>) -> Query {
|
||||
query.body = match query.body {
|
||||
SetExpr::Select(select) => SetExpr::Select(Box::new(modify_select(select, expressions))),
|
||||
_ => SetExpr::Select(Box::new(create_select(expressions))),
|
||||
@ -89,18 +93,18 @@ fn modify_query(mut query: Query, expressions: Vec<String>) -> Query {
|
||||
query
|
||||
}
|
||||
|
||||
fn modify_select(select: Box<Select>, expressions: Vec<String>) -> Select {
|
||||
fn modify_select(select: Box<Select>, projection: Vec<SelectItem>) -> Select {
|
||||
Select {
|
||||
projection: create_projection(expressions),
|
||||
projection,
|
||||
..select.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_select(expressions: Vec<String>) -> Select {
|
||||
fn create_select(projection: Vec<SelectItem>) -> Select {
|
||||
Select {
|
||||
distinct: false,
|
||||
top: None,
|
||||
projection: create_projection(expressions),
|
||||
projection,
|
||||
into: None,
|
||||
from: Vec::new(),
|
||||
lateral_views: Vec::new(),
|
||||
@ -112,20 +116,3 @@ fn create_select(expressions: Vec<String>) -> Select {
|
||||
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()
|
||||
}
|
||||
|
76
crates/nu-command/src/database/commands/testing.rs
Normal file
76
crates/nu-command/src/database/commands/testing.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape,
|
||||
Value,
|
||||
};
|
||||
use sqlparser::dialect::GenericDialect;
|
||||
use sqlparser::parser::Parser;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestingDb;
|
||||
|
||||
impl Command for TestingDb {
|
||||
fn name(&self) -> &str {
|
||||
"db testing"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
"query",
|
||||
SyntaxShape::String,
|
||||
"SQL to execute to create the query object",
|
||||
)
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Create query object"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "",
|
||||
example: "",
|
||||
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 dialect = GenericDialect {}; // or AnsiDialect, or your own dialect ...
|
||||
|
||||
let ast = Parser::parse_sql(&dialect, sql.item.as_str()).map_err(|e| {
|
||||
ShellError::GenericError(
|
||||
"Error creating AST".into(),
|
||||
e.to_string(),
|
||||
Some(sql.span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let value = match ast.get(0) {
|
||||
None => Value::nothing(call.head),
|
||||
Some(statement) => Value::String {
|
||||
val: format!("{:#?}", statement),
|
||||
span: call.head,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(value.into_pipeline_data())
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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()?,
|
||||
)),
|
||||
}
|
||||
}
|
71
crates/nu-command/src/database/expressions/alias.rs
Normal file
71
crates/nu-command/src/database/expressions/alias.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use crate::database::values::dsl::SelectDb;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape,
|
||||
};
|
||||
use sqlparser::ast::{Ident, SelectItem};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AliasExpr;
|
||||
|
||||
impl Command for AliasExpr {
|
||||
fn name(&self) -> &str {
|
||||
"db as"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required("alias", SyntaxShape::String, "alias name")
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Creates an alias for a column selection"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Creates an alias for a a column selection",
|
||||
example: "db col name_a | db as new_a",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "column", "expression"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let alias: String = call.req(engine_state, stack, 0)?;
|
||||
let select = SelectDb::try_from_pipeline(input, call.head)?;
|
||||
|
||||
let select = match select.into_native() {
|
||||
SelectItem::UnnamedExpr(expr) => SelectItem::ExprWithAlias {
|
||||
expr,
|
||||
alias: Ident {
|
||||
value: alias,
|
||||
quote_style: None,
|
||||
},
|
||||
},
|
||||
SelectItem::ExprWithAlias { expr, .. } => SelectItem::ExprWithAlias {
|
||||
expr,
|
||||
alias: Ident {
|
||||
value: alias,
|
||||
quote_style: None,
|
||||
},
|
||||
},
|
||||
select => select,
|
||||
};
|
||||
|
||||
let select: SelectDb = select.into();
|
||||
Ok(select.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
71
crates/nu-command/src/database/expressions/col.rs
Normal file
71
crates/nu-command/src/database/expressions/col.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use crate::database::values::dsl::{ExprDb, SelectDb};
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
|
||||
};
|
||||
use sqlparser::ast::{Ident, ObjectName, SelectItem};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ColExpr;
|
||||
|
||||
impl Command for ColExpr {
|
||||
fn name(&self) -> &str {
|
||||
"db col"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required("name", SyntaxShape::String, "column name")
|
||||
.category(Category::Custom("database".into()))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Creates column expression for database"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Creates a named column expression",
|
||||
example: "col name_1",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["database", "column", "expression"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let name: Value = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let select = match name {
|
||||
Value::String { val, .. } if val == "*" => SelectItem::Wildcard,
|
||||
Value::String { val, .. } if val.contains('.') => {
|
||||
let values = val
|
||||
.split('.')
|
||||
.map(|part| Ident {
|
||||
value: part.to_string(),
|
||||
quote_style: None,
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
SelectItem::QualifiedWildcard(ObjectName(values))
|
||||
}
|
||||
_ => {
|
||||
let expr = ExprDb::try_from_value(name)?;
|
||||
SelectItem::UnnamedExpr(expr.into_native())
|
||||
}
|
||||
};
|
||||
|
||||
let selection: SelectDb = select.into();
|
||||
Ok(selection.into_value(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
21
crates/nu-command/src/database/expressions/mod.rs
Normal file
21
crates/nu-command/src/database/expressions/mod.rs
Normal file
@ -0,0 +1,21 @@
|
||||
mod alias;
|
||||
mod col;
|
||||
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
|
||||
use alias::AliasExpr;
|
||||
use col::ColExpr;
|
||||
|
||||
pub fn add_expression_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!(AliasExpr, ColExpr);
|
||||
}
|
@ -1,8 +1,16 @@
|
||||
mod commands;
|
||||
mod values;
|
||||
|
||||
pub use commands::add_database_decls;
|
||||
mod expressions;
|
||||
pub use commands::add_commands_decls;
|
||||
pub use expressions::add_expression_decls;
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
pub use values::{
|
||||
convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_and_read_sqlite_db,
|
||||
open_connection_in_memory, read_sqlite_db, SQLiteDatabase,
|
||||
};
|
||||
|
||||
pub fn add_database_decls(working_set: &mut StateWorkingSet) {
|
||||
add_commands_decls(working_set);
|
||||
add_expression_decls(working_set);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_table::DbTable;
|
||||
use super::db_table::DbTable;
|
||||
|
||||
// Thank you gobang
|
||||
// https://github.com/TaKO8Ki/gobang/blob/main/database-tree/src/lib.rs
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_row::DbRow;
|
||||
use crate::database::values::definitions::db_row::DbRow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbColumn {
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_row::DbRow;
|
||||
use super::db_row::DbRow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbConstraint {
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_row::DbRow;
|
||||
use super::db_row::DbRow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbForeignKey {
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_row::DbRow;
|
||||
use super::db_row::DbRow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbIndex {
|
@ -1,4 +1,4 @@
|
||||
use crate::database::values::db_table::DbTable;
|
||||
use super::db_table::DbTable;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct DbSchema {
|
8
crates/nu-command/src/database/values/definitions/mod.rs
Normal file
8
crates/nu-command/src/database/values/definitions/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod db;
|
||||
pub mod db_column;
|
||||
pub mod db_constraint;
|
||||
pub mod db_foreignkey;
|
||||
pub mod db_index;
|
||||
pub mod db_row;
|
||||
pub mod db_schema;
|
||||
pub mod db_table;
|
160
crates/nu-command/src/database/values/dsl/expression.rs
Normal file
160
crates/nu-command/src/database/values/dsl/expression.rs
Normal file
@ -0,0 +1,160 @@
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use sqlparser::ast::{Expr, Ident};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ExprDb(Expr);
|
||||
|
||||
// Referenced access to the native expression
|
||||
impl AsRef<Expr> for ExprDb {
|
||||
fn as_ref(&self) -> &Expr {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<Expr> for ExprDb {
|
||||
fn as_mut(&mut self) -> &mut Expr {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Expr> for ExprDb {
|
||||
fn from(expr: Expr) -> Self {
|
||||
Self(expr)
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomValue for ExprDb {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
let cloned = Self(self.0.clone());
|
||||
|
||||
Value::CustomValue {
|
||||
val: Box::new(cloned),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
self.typetag_name().to_string()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
Ok(self.to_value(span))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
"DB expresssion"
|
||||
}
|
||||
|
||||
fn typetag_deserialize(&self) {
|
||||
unimplemented!("typetag_deserialize")
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprDb {
|
||||
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
|
||||
match value {
|
||||
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
|
||||
Some(expr) => Ok(Self(expr.0.clone())),
|
||||
None => Err(ShellError::CantConvert(
|
||||
"db expression".into(),
|
||||
"non-expression".into(),
|
||||
span,
|
||||
None,
|
||||
)),
|
||||
},
|
||||
Value::String { val, .. } => Ok(Expr::Identifier(Ident {
|
||||
value: val,
|
||||
quote_style: None,
|
||||
})
|
||||
.into()),
|
||||
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 into_native(self) -> Expr {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn to_value(&self, span: Span) -> Value {
|
||||
ExprDb::expr_to_value(self.as_ref(), span)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprDb {
|
||||
pub fn expr_to_value(expr: &Expr, span: Span) -> Value {
|
||||
match expr {
|
||||
Expr::Identifier(ident) => {
|
||||
let cols = vec!["value".into(), "quoted_style".into()];
|
||||
let val = Value::String {
|
||||
val: ident.value.to_string(),
|
||||
span,
|
||||
};
|
||||
let style = Value::String {
|
||||
val: format!("{:?}", ident.quote_style),
|
||||
span,
|
||||
};
|
||||
|
||||
Value::Record {
|
||||
cols,
|
||||
vals: vec![val, style],
|
||||
span,
|
||||
}
|
||||
}
|
||||
Expr::CompoundIdentifier(_) => todo!(),
|
||||
Expr::IsNull(_) => todo!(),
|
||||
Expr::IsNotNull(_) => todo!(),
|
||||
Expr::IsDistinctFrom(_, _) => todo!(),
|
||||
Expr::IsNotDistinctFrom(_, _) => todo!(),
|
||||
Expr::InList { .. } => todo!(),
|
||||
Expr::InSubquery { .. } => todo!(),
|
||||
Expr::InUnnest { .. } => todo!(),
|
||||
Expr::Between { .. } => todo!(),
|
||||
Expr::BinaryOp { .. } => todo!(),
|
||||
Expr::UnaryOp { .. } => todo!(),
|
||||
Expr::Cast { .. } => todo!(),
|
||||
Expr::TryCast { .. } => todo!(),
|
||||
Expr::Extract { .. } => todo!(),
|
||||
Expr::Substring { .. } => todo!(),
|
||||
Expr::Trim { .. } => todo!(),
|
||||
Expr::Collate { .. } => todo!(),
|
||||
Expr::Nested(_) => todo!(),
|
||||
Expr::Value(_) => todo!(),
|
||||
Expr::TypedString { .. } => todo!(),
|
||||
Expr::MapAccess { .. } => todo!(),
|
||||
Expr::Function(_) => todo!(),
|
||||
Expr::Case { .. } => todo!(),
|
||||
Expr::Exists(_) => todo!(),
|
||||
Expr::Subquery(_) => todo!(),
|
||||
Expr::ListAgg(_) => todo!(),
|
||||
Expr::GroupingSets(_) => todo!(),
|
||||
Expr::Cube(_) => todo!(),
|
||||
Expr::Rollup(_) => todo!(),
|
||||
Expr::Tuple(_) => todo!(),
|
||||
Expr::ArrayIndex { .. } => todo!(),
|
||||
Expr::Array(_) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
5
crates/nu-command/src/database/values/dsl/mod.rs
Normal file
5
crates/nu-command/src/database/values/dsl/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod expression;
|
||||
mod select_item;
|
||||
|
||||
pub(crate) use expression::ExprDb;
|
||||
pub(crate) use select_item::SelectDb;
|
222
crates/nu-command/src/database/values/dsl/select_item.rs
Normal file
222
crates/nu-command/src/database/values/dsl/select_item.rs
Normal file
@ -0,0 +1,222 @@
|
||||
use super::ExprDb;
|
||||
use nu_protocol::{ast::PathMember, CustomValue, PipelineData, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlparser::ast::{Expr, Ident, SelectItem};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SelectDb(SelectItem);
|
||||
|
||||
// Referenced access to the native expression
|
||||
impl AsRef<SelectItem> for SelectDb {
|
||||
fn as_ref(&self) -> &SelectItem {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<SelectItem> for SelectDb {
|
||||
fn as_mut(&mut self) -> &mut SelectItem {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SelectItem> for SelectDb {
|
||||
fn from(expr: SelectItem) -> Self {
|
||||
Self(expr)
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomValue for SelectDb {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
let cloned = Self(self.0.clone());
|
||||
|
||||
Value::CustomValue {
|
||||
val: Box::new(cloned),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
self.typetag_name().to_string()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
Ok(self.to_value(span))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn follow_path_int(&self, count: usize, span: Span) -> Result<Value, ShellError> {
|
||||
let path = PathMember::Int { val: count, span };
|
||||
|
||||
SelectDb::select_to_value(self.as_ref(), span).follow_cell_path(&[path])
|
||||
}
|
||||
|
||||
fn follow_path_string(&self, column_name: String, span: Span) -> Result<Value, ShellError> {
|
||||
let path = PathMember::String {
|
||||
val: column_name,
|
||||
span,
|
||||
};
|
||||
SelectDb::select_to_value(self.as_ref(), span).follow_cell_path(&[path])
|
||||
}
|
||||
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
"DB selection"
|
||||
}
|
||||
|
||||
fn typetag_deserialize(&self) {
|
||||
unimplemented!("typetag_deserialize")
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectDb {
|
||||
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
|
||||
match value {
|
||||
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
|
||||
Some(expr) => Ok(Self(expr.0.clone())),
|
||||
None => Err(ShellError::CantConvert(
|
||||
"db expression".into(),
|
||||
"non-expression".into(),
|
||||
span,
|
||||
None,
|
||||
)),
|
||||
},
|
||||
Value::String { val, .. } => {
|
||||
let expr = Expr::Identifier(Ident {
|
||||
value: val,
|
||||
quote_style: None,
|
||||
});
|
||||
|
||||
Ok(SelectItem::UnnamedExpr(expr).into())
|
||||
}
|
||||
x => Err(ShellError::CantConvert(
|
||||
"selection".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 into_native(self) -> SelectItem {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn to_value(&self, span: Span) -> Value {
|
||||
SelectDb::select_to_value(self.as_ref(), span)
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectDb {
|
||||
fn select_to_value(select: &SelectItem, span: Span) -> Value {
|
||||
match select {
|
||||
SelectItem::UnnamedExpr(expr) => ExprDb::expr_to_value(expr, span),
|
||||
SelectItem::ExprWithAlias { expr, alias } => {
|
||||
let expr = ExprDb::expr_to_value(expr, span);
|
||||
|
||||
let val = Value::String {
|
||||
val: alias.value.to_string(),
|
||||
span,
|
||||
};
|
||||
let style = Value::String {
|
||||
val: format!("{:?}", alias.quote_style),
|
||||
span,
|
||||
};
|
||||
|
||||
let cols = vec!["value".into(), "quoted_style".into()];
|
||||
let alias = Value::Record {
|
||||
cols,
|
||||
vals: vec![val, style],
|
||||
span,
|
||||
};
|
||||
|
||||
let cols = vec!["expression".into(), "alias".into()];
|
||||
Value::Record {
|
||||
cols,
|
||||
vals: vec![expr, alias],
|
||||
span,
|
||||
}
|
||||
}
|
||||
SelectItem::QualifiedWildcard(object) => {
|
||||
let vals: Vec<Value> = object
|
||||
.0
|
||||
.iter()
|
||||
.map(|ident| Value::String {
|
||||
val: ident.value.clone(),
|
||||
span,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Value::List { vals, span }
|
||||
}
|
||||
SelectItem::Wildcard => Value::String {
|
||||
val: "*".into(),
|
||||
span,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Convenient function to extrac multiple SelectItem that could be inside a
|
||||
// nushell Value
|
||||
pub fn extract_selects(value: Value) -> Result<Vec<SelectItem>, ShellError> {
|
||||
ExtractedSelect::extract_selects(value).map(ExtractedSelect::into_selects)
|
||||
}
|
||||
}
|
||||
|
||||
// Enum to represent the parsing of the selects from Value
|
||||
enum ExtractedSelect {
|
||||
Single(SelectItem),
|
||||
List(Vec<ExtractedSelect>),
|
||||
}
|
||||
|
||||
impl ExtractedSelect {
|
||||
fn into_selects(self) -> Vec<SelectItem> {
|
||||
match self {
|
||||
Self::Single(select) => vec![select],
|
||||
Self::List(selects) => selects
|
||||
.into_iter()
|
||||
.flat_map(ExtractedSelect::into_selects)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_selects(value: Value) -> Result<ExtractedSelect, ShellError> {
|
||||
match value {
|
||||
Value::String { val, .. } => {
|
||||
let expr = Expr::Identifier(Ident {
|
||||
value: val,
|
||||
quote_style: None,
|
||||
});
|
||||
|
||||
Ok(ExtractedSelect::Single(SelectItem::UnnamedExpr(expr)))
|
||||
}
|
||||
Value::CustomValue { .. } => SelectDb::try_from_value(value)
|
||||
.map(SelectDb::into_native)
|
||||
.map(ExtractedSelect::Single),
|
||||
Value::List { vals, .. } => vals
|
||||
.into_iter()
|
||||
.map(Self::extract_selects)
|
||||
.collect::<Result<Vec<ExtractedSelect>, ShellError>>()
|
||||
.map(ExtractedSelect::List),
|
||||
x => Err(ShellError::CantConvert(
|
||||
"expression".into(),
|
||||
x.get_type().to_string(),
|
||||
x.span()?,
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,5 @@
|
||||
pub mod db;
|
||||
pub mod db_column;
|
||||
pub mod db_constraint;
|
||||
pub mod db_foreignkey;
|
||||
pub mod db_index;
|
||||
pub mod db_row;
|
||||
pub mod db_schema;
|
||||
pub mod db_table;
|
||||
pub mod definitions;
|
||||
pub mod dsl;
|
||||
pub mod sqlite;
|
||||
|
||||
pub use sqlite::{
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::database::values::{
|
||||
use crate::database::values::definitions::{
|
||||
db::Db, db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey,
|
||||
db_index::DbIndex, db_table::DbTable,
|
||||
};
|
||||
use nu_protocol::{CustomValue, PipelineData, ShellError, Span, Spanned, Value};
|
||||
use rusqlite::{types::ValueRef, Connection, Row};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlparser::ast::Query;
|
||||
use std::{
|
||||
fs::File,
|
||||
@ -14,7 +14,7 @@ use std::{
|
||||
|
||||
const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes();
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
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
|
||||
@ -23,27 +23,6 @@ pub struct SQLiteDatabase {
|
||||
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) -> Self {
|
||||
Self {
|
||||
@ -53,42 +32,56 @@ impl SQLiteDatabase {
|
||||
}
|
||||
|
||||
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 file =
|
||||
File::open(path).map_err(|e| ShellError::ReadingFile(e.to_string(), span))?;
|
||||
|
||||
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(),
|
||||
)
|
||||
})
|
||||
.map_err(|e| ShellError::ReadingFile(e.to_string(), span))
|
||||
.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(),
|
||||
))
|
||||
Err(ShellError::ReadingFile("Not a SQLite file".into(), span))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 query(&self, sql: &Spanned<String>, call_span: Span) -> Result<Value, ShellError> {
|
||||
let db = open_sqlite_db(&self.path, call_span)?;
|
||||
run_sql_query(db, sql).map_err(|e| {
|
||||
@ -131,41 +124,6 @@ impl SQLiteDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -532,6 +490,7 @@ fn read_entire_sqlite_db(conn: Connection, call_span: Span) -> Result<Value, rus
|
||||
span: call_span,
|
||||
})
|
||||
}
|
||||
|
||||
pub 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();
|
||||
|
@ -545,6 +545,15 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
|
||||
#[error("No file to be copied")]
|
||||
NoFileToBeCopied(),
|
||||
|
||||
/// Error while trying to read a file
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// The error will show the result from a file operation
|
||||
#[error("Error trying to read file")]
|
||||
#[diagnostic(code(nu::shell::error_reading_file), url(docsrs))]
|
||||
ReadingFile(String, #[label("{0}")] Span),
|
||||
|
||||
/// A name was not found. Did you mean a different name?
|
||||
///
|
||||
/// ## Resolution
|
||||
|
Loading…
Reference in New Issue
Block a user