diff --git a/Cargo.lock b/Cargo.lock index 29215ff052..7dc2322f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4102,6 +4102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e9a527b68048eb95495a1508f6c8395c8defcff5ecdbe8ad4106d08a2ef2a3c" dependencies = [ "log", + "serde", ] [[package]] diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 4a7d1aa8b9..58a367972a 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -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" diff --git a/crates/nu-command/src/database/commands/mod.rs b/crates/nu-command/src/database/commands/mod.rs index 9f4a053ba1..99aaa74881 100644 --- a/crates/nu-command/src/database/commands/mod.rs +++ b/crates/nu-command/src/database/commands/mod.rs @@ -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 + ); } diff --git a/crates/nu-command/src/database/commands/schema.rs b/crates/nu-command/src/database/commands/schema.rs index ba6e2c89d2..aa517569cd 100644 --- a/crates/nu-command/src/database/commands/schema.rs +++ b/crates/nu-command/src/database/commands/schema.rs @@ -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}, diff --git a/crates/nu-command/src/database/commands/select.rs b/crates/nu-command/src/database/commands/select.rs index a52be24290..013c099521 100644 --- a/crates/nu-command/src/database/commands/select.rs +++ b/crates/nu-command/src/database/commands/select.rs @@ -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 { - let value: Value = call.req(engine_state, stack, 0)?; - let expressions = extract_strings(value)?; + let vals: Vec = 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) -> Query { +fn create_query(expressions: Vec) -> Query { Query { with: None, body: SetExpr::Select(Box::new(create_select(expressions))), @@ -80,7 +84,7 @@ fn create_query(expressions: Vec) -> Query { } } -fn modify_query(mut query: Query, expressions: Vec) -> Query { +fn modify_query(mut query: Query, expressions: Vec) -> 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) -> Query { query } -fn modify_select(select: Box, projection: Vec) -> Select { Select { - projection: create_projection(expressions), + projection, ..select.as_ref().clone() } } -fn create_select(expressions: Vec) -> Select { +fn create_select(projection: Vec) -> 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) -> 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) -> Vec { - expressions - .into_iter() - .map(|expression| { - let expr = Expr::Identifier(Ident { - value: expression, - quote_style: None, - }); - - SelectItem::UnnamedExpr(expr) - }) - .collect() -} diff --git a/crates/nu-command/src/database/commands/testing.rs b/crates/nu-command/src/database/commands/testing.rs new file mode 100644 index 0000000000..84451cf721 --- /dev/null +++ b/crates/nu-command/src/database/commands/testing.rs @@ -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 { + 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 { + let sql: Spanned = 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()) + } +} diff --git a/crates/nu-command/src/database/commands/utils.rs b/crates/nu-command/src/database/commands/utils.rs deleted file mode 100644 index 58f7589f63..0000000000 --- a/crates/nu-command/src/database/commands/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use nu_protocol::{FromValue, ShellError, Value}; - -pub fn extract_strings(value: Value) -> Result, ShellError> { - match ( - ::from_value(&value), - 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()?, - )), - } -} diff --git a/crates/nu-command/src/database/expressions/alias.rs b/crates/nu-command/src/database/expressions/alias.rs new file mode 100644 index 0000000000..a630437be0 --- /dev/null +++ b/crates/nu-command/src/database/expressions/alias.rs @@ -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 { + 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 { + 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()) + } +} diff --git a/crates/nu-command/src/database/expressions/col.rs b/crates/nu-command/src/database/expressions/col.rs new file mode 100644 index 0000000000..02b5727861 --- /dev/null +++ b/crates/nu-command/src/database/expressions/col.rs @@ -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 { + 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 { + 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::>(); + + 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()) + } +} diff --git a/crates/nu-command/src/database/expressions/mod.rs b/crates/nu-command/src/database/expressions/mod.rs new file mode 100644 index 0000000000..0f7a7d3c2e --- /dev/null +++ b/crates/nu-command/src/database/expressions/mod.rs @@ -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); +} diff --git a/crates/nu-command/src/database/mod.rs b/crates/nu-command/src/database/mod.rs index 5a11974a8c..52fd929e67 100644 --- a/crates/nu-command/src/database/mod.rs +++ b/crates/nu-command/src/database/mod.rs @@ -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); +} diff --git a/crates/nu-command/src/database/values/db.rs b/crates/nu-command/src/database/values/definitions/db.rs similarity index 91% rename from crates/nu-command/src/database/values/db.rs rename to crates/nu-command/src/database/values/definitions/db.rs index 1b9dd5c46b..fc8acfcfb7 100644 --- a/crates/nu-command/src/database/values/db.rs +++ b/crates/nu-command/src/database/values/definitions/db.rs @@ -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 diff --git a/crates/nu-command/src/database/values/db_column.rs b/crates/nu-command/src/database/values/definitions/db_column.rs similarity index 96% rename from crates/nu-command/src/database/values/db_column.rs rename to crates/nu-command/src/database/values/definitions/db_column.rs index 03010ad2be..61f27ba195 100644 --- a/crates/nu-command/src/database/values/db_column.rs +++ b/crates/nu-command/src/database/values/definitions/db_column.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 { diff --git a/crates/nu-command/src/database/values/db_constraint.rs b/crates/nu-command/src/database/values/definitions/db_constraint.rs similarity index 92% rename from crates/nu-command/src/database/values/db_constraint.rs rename to crates/nu-command/src/database/values/definitions/db_constraint.rs index e067856fbf..6a1a8def6a 100644 --- a/crates/nu-command/src/database/values/db_constraint.rs +++ b/crates/nu-command/src/database/values/definitions/db_constraint.rs @@ -1,4 +1,4 @@ -use crate::database::values::db_row::DbRow; +use super::db_row::DbRow; #[derive(Debug)] pub struct DbConstraint { diff --git a/crates/nu-command/src/database/values/db_foreignkey.rs b/crates/nu-command/src/database/values/definitions/db_foreignkey.rs similarity index 94% rename from crates/nu-command/src/database/values/db_foreignkey.rs rename to crates/nu-command/src/database/values/definitions/db_foreignkey.rs index 97b839b5c7..1797cc1ff2 100644 --- a/crates/nu-command/src/database/values/db_foreignkey.rs +++ b/crates/nu-command/src/database/values/definitions/db_foreignkey.rs @@ -1,4 +1,4 @@ -use crate::database::values::db_row::DbRow; +use super::db_row::DbRow; #[derive(Debug)] pub struct DbForeignKey { diff --git a/crates/nu-command/src/database/values/db_index.rs b/crates/nu-command/src/database/values/definitions/db_index.rs similarity index 94% rename from crates/nu-command/src/database/values/db_index.rs rename to crates/nu-command/src/database/values/definitions/db_index.rs index 98a6ed9354..dc3366b10f 100644 --- a/crates/nu-command/src/database/values/db_index.rs +++ b/crates/nu-command/src/database/values/definitions/db_index.rs @@ -1,4 +1,4 @@ -use crate::database::values::db_row::DbRow; +use super::db_row::DbRow; #[derive(Debug)] pub struct DbIndex { diff --git a/crates/nu-command/src/database/values/db_row.rs b/crates/nu-command/src/database/values/definitions/db_row.rs similarity index 100% rename from crates/nu-command/src/database/values/db_row.rs rename to crates/nu-command/src/database/values/definitions/db_row.rs diff --git a/crates/nu-command/src/database/values/db_schema.rs b/crates/nu-command/src/database/values/definitions/db_schema.rs similarity index 70% rename from crates/nu-command/src/database/values/db_schema.rs rename to crates/nu-command/src/database/values/definitions/db_schema.rs index 55fbe7409d..e2ef9bc1bd 100644 --- a/crates/nu-command/src/database/values/db_schema.rs +++ b/crates/nu-command/src/database/values/definitions/db_schema.rs @@ -1,4 +1,4 @@ -use crate::database::values::db_table::DbTable; +use super::db_table::DbTable; #[derive(Clone, PartialEq, Debug)] pub struct DbSchema { diff --git a/crates/nu-command/src/database/values/db_table.rs b/crates/nu-command/src/database/values/definitions/db_table.rs similarity index 100% rename from crates/nu-command/src/database/values/db_table.rs rename to crates/nu-command/src/database/values/definitions/db_table.rs diff --git a/crates/nu-command/src/database/values/definitions/mod.rs b/crates/nu-command/src/database/values/definitions/mod.rs new file mode 100644 index 0000000000..51a4dfc0dd --- /dev/null +++ b/crates/nu-command/src/database/values/definitions/mod.rs @@ -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; diff --git a/crates/nu-command/src/database/values/dsl/expression.rs b/crates/nu-command/src/database/values/dsl/expression.rs new file mode 100644 index 0000000000..4dbe6ef1de --- /dev/null +++ b/crates/nu-command/src/database/values/dsl/expression.rs @@ -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 for ExprDb { + fn as_ref(&self) -> &Expr { + &self.0 + } +} + +impl AsMut for ExprDb { + fn as_mut(&mut self) -> &mut Expr { + &mut self.0 + } +} + +impl From 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 { + 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 { + match value { + Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { + 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 { + // 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!(), + } + } +} diff --git a/crates/nu-command/src/database/values/dsl/mod.rs b/crates/nu-command/src/database/values/dsl/mod.rs new file mode 100644 index 0000000000..042af5abbd --- /dev/null +++ b/crates/nu-command/src/database/values/dsl/mod.rs @@ -0,0 +1,5 @@ +mod expression; +mod select_item; + +pub(crate) use expression::ExprDb; +pub(crate) use select_item::SelectDb; diff --git a/crates/nu-command/src/database/values/dsl/select_item.rs b/crates/nu-command/src/database/values/dsl/select_item.rs new file mode 100644 index 0000000000..8c4020c015 --- /dev/null +++ b/crates/nu-command/src/database/values/dsl/select_item.rs @@ -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 for SelectDb { + fn as_ref(&self) -> &SelectItem { + &self.0 + } +} + +impl AsMut for SelectDb { + fn as_mut(&mut self) -> &mut SelectItem { + &mut self.0 + } +} + +impl From 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 { + Ok(self.to_value(span)) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn follow_path_int(&self, count: usize, span: Span) -> Result { + 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 { + 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 { + match value { + Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { + 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 { + 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 = 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, 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), +} + +impl ExtractedSelect { + fn into_selects(self) -> Vec { + 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 { + 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::, ShellError>>() + .map(ExtractedSelect::List), + x => Err(ShellError::CantConvert( + "expression".into(), + x.get_type().to_string(), + x.span()?, + None, + )), + } + } +} diff --git a/crates/nu-command/src/database/values/mod.rs b/crates/nu-command/src/database/values/mod.rs index 5a941a750c..2a3673f79e 100644 --- a/crates/nu-command/src/database/values/mod.rs +++ b/crates/nu-command/src/database/values/mod.rs @@ -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::{ diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index 48d7ed7aa7..0fb14cfcf0 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -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, } -// Mocked serialization of the object -impl Serialize for SQLiteDatabase { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_none() - } -} - -// Mocked deserialization of the object -impl<'de> Deserialize<'de> for SQLiteDatabase { - fn deserialize(_deserializer: D) -> Result - 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 { - 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 { + match value { + Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { + 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 { + 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, call_span: Span) -> Result { 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 { - match value { - Value::CustomValue { val, span } => match val.as_any().downcast_ref::() { - 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 { - 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 { let mut vals = Vec::new(); let colnamestr = row.as_ref().column_names().to_vec(); diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 1f7f34ba6c..97fb9bb46a 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -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