diff --git a/crates/nu-command/src/database/commands/and.rs b/crates/nu-command/src/database/commands/and.rs new file mode 100644 index 0000000000..45cdbc6094 --- /dev/null +++ b/crates/nu-command/src/database/commands/and.rs @@ -0,0 +1,143 @@ +use crate::database::values::dsl::ExprDb; + +use super::super::SQLiteDatabase; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; +use sqlparser::ast::{BinaryOperator, Expr, Query, Select, SetExpr}; + +#[derive(Clone)] +pub struct AndDb; + +impl Command for AndDb { + fn name(&self) -> &str { + "db and" + } + + fn usage(&self) -> &str { + "Includes an AND clause for a query or expression" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("where", SyntaxShape::Any, "Where expression on the table") + .category(Category::Custom("database".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "where"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "selects a column from a database with a where clause", + example: r#"db open db.mysql + | db select a + | db from table_1 + | db where ((db col a) > 1) + | db and ((db col b) == 1) + | db describe"#, + result: None, + }, + Example { + description: "Creates a nested where clause", + example: r#"db open db.mysql + | db select a + | db from table_1 + | db where ((db col a) > 1 | db and ((db col a) < 10)) + | db describe"#, + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + let expr = ExprDb::try_from_value(&value)?.into_native(); + + let value = input.into_value(call.head); + if let Ok(expression) = ExprDb::try_from_value(&value) { + let expression = Expr::BinaryOp { + left: Box::new(expression.into_native()), + op: BinaryOperator::And, + right: Box::new(expr), + }; + + let expression: ExprDb = Expr::Nested(Box::new(expression)).into(); + + Ok(expression.into_value(call.head).into_pipeline_data()) + } else if let Ok(mut db) = SQLiteDatabase::try_from_value(value.clone()) { + db.query = match db.query { + Some(query) => Some(modify_query(query, expr, call.head)?), + None => { + return Err(ShellError::GenericError( + "Connection without query".into(), + "Missing query in the connection".into(), + Some(call.head), + None, + Vec::new(), + )) + } + }; + + Ok(db.into_value(call.head).into_pipeline_data()) + } else { + Err(ShellError::CantConvert( + "expression or query".into(), + value.get_type().to_string(), + value.span()?, + None, + )) + } + } +} + +fn modify_query(mut query: Query, expression: Expr, span: Span) -> Result { + query.body = match query.body { + SetExpr::Select(select) => Ok(SetExpr::Select(modify_select(select, expression, span)?)), + _ => Err(ShellError::GenericError( + "Query without a select".into(), + "Missing a WHERE clause before an AND clause".into(), + Some(span), + None, + Vec::new(), + )), + }?; + + Ok(query) +} + +fn modify_select( + mut select: Box, table: String) -> Select { - Select { - from: create_from(table), - ..select.as_ref().clone() - } +fn modify_select(mut select: Box { + select.as_mut().from = create_from(table); + select } fn create_select(table: String) -> Select { diff --git a/crates/nu-command/src/database/commands/limit.rs b/crates/nu-command/src/database/commands/limit.rs new file mode 100644 index 0000000000..d2b7f14558 --- /dev/null +++ b/crates/nu-command/src/database/commands/limit.rs @@ -0,0 +1,77 @@ +use super::super::SQLiteDatabase; +use crate::database::values::dsl::ExprDb; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct LimitDb; + +impl Command for LimitDb { + fn name(&self) -> &str { + "db limit" + } + + fn usage(&self) -> &str { + "Limit result from query" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "limit", + SyntaxShape::Int, + "Number of rows to extract for query", + ) + .category(Category::Custom("database".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "limit"] + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Limits selection from table", + example: r#"db open db.mysql + | db from table_a + | db select a + | db limit 10 + | db describe"#, + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let limit: Value = call.req(engine_state, stack, 0)?; + let expr = ExprDb::try_from_value(&limit)?.into_native(); + + let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?; + db.query = match db.query { + Some(mut query) => { + query.limit = Some(expr); + Some(query) + } + None => { + return Err(ShellError::GenericError( + "Connection without query".into(), + "The connection needs a query defined".into(), + Some(call.head), + None, + Vec::new(), + )) + } + }; + + Ok(db.into_value(call.head).into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/database/commands/mod.rs b/crates/nu-command/src/database/commands/mod.rs index 99aaa74881..7906918319 100644 --- a/crates/nu-command/src/database/commands/mod.rs +++ b/crates/nu-command/src/database/commands/mod.rs @@ -1,11 +1,16 @@ +mod and; mod collect; mod command; mod describe; mod from; +mod limit; mod open; +mod or; +mod order_by; mod query; mod schema; mod select; +mod where_; // Temporal module to create Query objects mod testing; @@ -13,14 +18,19 @@ use testing::TestingDb; use nu_protocol::engine::StateWorkingSet; +use and::AndDb; use collect::CollectDb; use command::Database; use describe::DescribeDb; use from::FromDb; +use limit::LimitDb; use open::OpenDb; +use or::OrDb; +use order_by::OrderByDb; use query::QueryDb; use schema::SchemaDb; use select::ProjectionDb; +use where_::WhereDb; pub fn add_commands_decls(working_set: &mut StateWorkingSet) { macro_rules! bind_command { @@ -34,14 +44,19 @@ pub fn add_commands_decls(working_set: &mut StateWorkingSet) { // Series commands bind_command!( + AndDb, CollectDb, Database, DescribeDb, FromDb, QueryDb, + LimitDb, ProjectionDb, OpenDb, + OrderByDb, + OrDb, SchemaDb, - TestingDb + TestingDb, + WhereDb ); } diff --git a/crates/nu-command/src/database/commands/open.rs b/crates/nu-command/src/database/commands/open.rs index e099423080..f2d6d1eb6c 100644 --- a/crates/nu-command/src/database/commands/open.rs +++ b/crates/nu-command/src/database/commands/open.rs @@ -31,8 +31,8 @@ impl Command for OpenDb { fn examples(&self) -> Vec { vec![Example { - description: "", - example: r#"""#, + description: "Open a sqlite file", + example: r#"db open file.sqlite"#, result: None, }] } diff --git a/crates/nu-command/src/database/commands/or.rs b/crates/nu-command/src/database/commands/or.rs new file mode 100644 index 0000000000..62ee26bbe4 --- /dev/null +++ b/crates/nu-command/src/database/commands/or.rs @@ -0,0 +1,143 @@ +use crate::database::values::dsl::ExprDb; + +use super::super::SQLiteDatabase; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Value, +}; +use sqlparser::ast::{BinaryOperator, Expr, Query, Select, SetExpr}; + +#[derive(Clone)] +pub struct OrDb; + +impl Command for OrDb { + fn name(&self) -> &str { + "db or" + } + + fn usage(&self) -> &str { + "Includes an OR clause for a query or expression" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("where", SyntaxShape::Any, "Where expression on the table") + .category(Category::Custom("database".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "where"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "selects a column from a database with a where clause", + example: r#"db open db.mysql + | db select a + | db from table_1 + | db where ((db col a) > 1) + | db or ((db col b) == 1) + | db describe"#, + result: None, + }, + Example { + description: "Creates a nested where clause", + example: r#"db open db.mysql + | db select a + | db from table_1 + | db where ((db col a) > 1 | db or ((db col a) < 10)) + | db describe"#, + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + let expr = ExprDb::try_from_value(&value)?.into_native(); + + let value = input.into_value(call.head); + if let Ok(expression) = ExprDb::try_from_value(&value) { + let expression = Expr::BinaryOp { + left: Box::new(expression.into_native()), + op: BinaryOperator::Or, + right: Box::new(expr), + }; + + let expression: ExprDb = Expr::Nested(Box::new(expression)).into(); + + Ok(expression.into_value(call.head).into_pipeline_data()) + } else if let Ok(mut db) = SQLiteDatabase::try_from_value(value.clone()) { + db.query = match db.query { + Some(query) => Some(modify_query(query, expr, call.head)?), + None => { + return Err(ShellError::GenericError( + "Connection without query".into(), + "Missing query in the connection".into(), + Some(call.head), + None, + Vec::new(), + )) + } + }; + + Ok(db.into_value(call.head).into_pipeline_data()) + } else { + Err(ShellError::CantConvert( + "expression or query".into(), + value.get_type().to_string(), + value.span()?, + None, + )) + } + } +} + +fn modify_query(mut query: Query, expression: Expr, span: Span) -> Result { + query.body = match query.body { + SetExpr::Select(select) => Ok(SetExpr::Select(modify_select(select, expression, span)?)), + _ => Err(ShellError::GenericError( + "Query without a select".into(), + "Missing a WHERE clause before an OR clause".into(), + Some(span), + None, + Vec::new(), + )), + }?; + + Ok(query) +} + +fn modify_select( + mut select: Box, projection: Vec) -> Select { - Select { - projection, - ..select.as_ref().clone() - } +fn modify_select(mut select: Box { + select.as_mut().projection = projection; + select } fn create_select(projection: Vec) -> Select { diff --git a/crates/nu-command/src/database/commands/where_.rs b/crates/nu-command/src/database/commands/where_.rs new file mode 100644 index 0000000000..47e6f9ed1e --- /dev/null +++ b/crates/nu-command/src/database/commands/where_.rs @@ -0,0 +1,103 @@ +use crate::database::values::dsl::ExprDb; + +use super::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, Query, Select, SetExpr}; + +#[derive(Clone)] +pub struct WhereDb; + +impl Command for WhereDb { + fn name(&self) -> &str { + "db where" + } + + fn usage(&self) -> &str { + "Includes a where statement for a query" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("where", SyntaxShape::Any, "Where expression on the table") + .category(Category::Custom("database".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "where"] + } + + fn examples(&self) -> Vec { + vec![Example { + description: "selects a column from a database with a where clause", + example: r#"db open db.mysql + | db select a + | db from table_1 + | db where ((db col a) > 1) + | db describe"#, + result: None, + }] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + let expr = ExprDb::try_from_value(&value)?.into_native(); + + let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?; + db.query = match db.query { + Some(query) => Some(modify_query(query, expr)), + None => { + return Err(ShellError::GenericError( + "Connection without query".into(), + "The connection needs a query defined".into(), + Some(call.head), + None, + Vec::new(), + )) + } + }; + + Ok(db.into_value(call.head).into_pipeline_data()) + } +} + +fn modify_query(mut query: Query, expression: Expr) -> Query { + query.body = match query.body { + SetExpr::Select(select) => SetExpr::Select(modify_select(select, expression)), + _ => SetExpr::Select(Box::new(create_select(expression))), + }; + + query +} + +fn modify_select(mut select: Box { + select.as_mut().selection = Some(expression); + select +} + +fn create_select(expression: Expr) -> Select { + Select { + distinct: false, + top: None, + into: None, + projection: Vec::new(), + from: Vec::new(), + lateral_views: Vec::new(), + selection: Some(expression), + group_by: Vec::new(), + cluster_by: Vec::new(), + distribute_by: Vec::new(), + sort_by: Vec::new(), + having: None, + } +} diff --git a/crates/nu-command/src/database/expressions/alias.rs b/crates/nu-command/src/database/expressions/alias.rs index a630437be0..9cda68a0ba 100644 --- a/crates/nu-command/src/database/expressions/alias.rs +++ b/crates/nu-command/src/database/expressions/alias.rs @@ -1,11 +1,14 @@ -use crate::database::values::dsl::SelectDb; +use crate::{ + database::values::dsl::{ExprDb, SelectDb}, + 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, SelectItem}; +use sqlparser::ast::{Ident, SelectItem, SetExpr, TableAlias, TableFactor}; #[derive(Clone)] pub struct AliasExpr; @@ -26,11 +29,22 @@ impl Command for AliasExpr { } 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, - }] + vec![ + Example { + description: "Creates an alias for a column selection", + example: "db col name_a | db as new_a", + result: None, + }, + Example { + description: "Creates an alias for a table", + example: r#"db open name + | db select a + | db from table_a + | db as table_a_new + | db describe"#, + result: None, + }, + ] } fn search_terms(&self) -> Vec<&str> { @@ -45,27 +59,95 @@ impl Command for AliasExpr { input: PipelineData, ) -> Result { let alias: String = call.req(engine_state, stack, 0)?; - let select = SelectDb::try_from_pipeline(input, call.head)?; + let value = input.into_value(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()) + if let Ok(expr) = ExprDb::try_from_value(&value) { + alias_selection(expr.into_native().into(), alias, call) + } else if let Ok(select) = SelectDb::try_from_value(&value) { + alias_selection(select, alias, call) + } else if let Ok(db) = SQLiteDatabase::try_from_value(value.clone()) { + alias_db(db, alias, call) + } else { + Err(ShellError::CantConvert( + "expression or query".into(), + value.get_type().to_string(), + value.span()?, + None, + )) + } + } +} + +fn alias_selection( + select: SelectDb, + alias: String, + call: &Call, +) -> Result { + 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()) +} + +fn alias_db( + mut db: SQLiteDatabase, + new_alias: String, + call: &Call, +) -> Result { + match db.query { + None => Err(ShellError::GenericError( + "Error creating alias".into(), + "there is no query defined yet".into(), + Some(call.head), + None, + Vec::new(), + )), + Some(ref mut query) => match &mut query.body { + SetExpr::Select(ref mut select) => { + select.as_mut().from.iter_mut().for_each(|table| { + let new_alias = Some(TableAlias { + name: Ident { + value: new_alias.clone(), + quote_style: None, + }, + columns: Vec::new(), + }); + + if let TableFactor::Table { ref mut alias, .. } = table.relation { + *alias = new_alias; + } else if let TableFactor::Derived { ref mut alias, .. } = table.relation { + *alias = new_alias; + } else if let TableFactor::TableFunction { ref mut alias, .. } = table.relation + { + *alias = new_alias; + } + }); + + Ok(db.into_value(call.head).into_pipeline_data()) + } + _ => Err(ShellError::GenericError( + "Error creating alias".into(), + "Query has no select from defined".into(), + Some(call.head), + None, + Vec::new(), + )), + }, } } diff --git a/crates/nu-command/src/database/expressions/col.rs b/crates/nu-command/src/database/expressions/col.rs index 02b5727861..f604d26b69 100644 --- a/crates/nu-command/src/database/expressions/col.rs +++ b/crates/nu-command/src/database/expressions/col.rs @@ -1,11 +1,10 @@ -use crate::database::values::dsl::{ExprDb, SelectDb}; +use crate::database::values::dsl::ExprDb; 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; @@ -44,28 +43,9 @@ impl Command for ColExpr { call: &Call, _input: PipelineData, ) -> Result { - let name: Value = call.req(engine_state, stack, 0)?; + let value: Value = call.req(engine_state, stack, 0)?; + let expression = ExprDb::try_from_value(&value)?; - 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()) + Ok(expression.into_value(call.head).into_pipeline_data()) } } diff --git a/crates/nu-command/src/database/values/dsl/expression.rs b/crates/nu-command/src/database/values/dsl/expression.rs index 4dbe6ef1de..896cf4d509 100644 --- a/crates/nu-command/src/database/values/dsl/expression.rs +++ b/crates/nu-command/src/database/values/dsl/expression.rs @@ -1,7 +1,9 @@ -use nu_protocol::{CustomValue, ShellError, Span, Value}; +use nu_protocol::{ + ast::{Operator, PathMember}, + CustomValue, ShellError, Span, Type, Value, +}; use serde::{Deserialize, Serialize}; - -use sqlparser::ast::{Expr, Ident}; +use sqlparser::ast::{BinaryOperator, Expr, Ident}; #[derive(Debug, Serialize, Deserialize)] pub struct ExprDb(Expr); @@ -47,6 +49,21 @@ impl CustomValue for ExprDb { self } + fn follow_path_int(&self, count: usize, span: Span) -> Result { + let path = PathMember::Int { val: count, span }; + + ExprDb::expr_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, + }; + + ExprDb::expr_to_value(self.as_ref(), span).follow_cell_path(&[path]) + } + fn typetag_name(&self) -> &'static str { "DB expresssion" } @@ -54,25 +71,86 @@ impl CustomValue for ExprDb { fn typetag_deserialize(&self) { unimplemented!("typetag_deserialize") } + + fn operation( + &self, + lhs_span: Span, + operator: Operator, + op: Span, + right: &Value, + ) -> Result { + let right_expr = match right { + Value::CustomValue { .. } => ExprDb::try_from_value(right).map(ExprDb::into_native), + Value::String { val, .. } => Ok(Expr::Value( + sqlparser::ast::Value::SingleQuotedString(val.clone()), + )), + Value::Int { val, .. } => Ok(Expr::Value(sqlparser::ast::Value::Number( + format!("{}", val), + false, + ))), + Value::Bool { val, .. } => Ok(Expr::Value(sqlparser::ast::Value::Boolean(*val))), + _ => Err(ShellError::OperatorMismatch { + op_span: op, + lhs_ty: Type::Custom, + lhs_span, + rhs_ty: right.get_type(), + rhs_span: right.span()?, + }), + }?; + + let sql_operator = match operator { + Operator::Equal => Ok(BinaryOperator::Eq), + Operator::NotEqual => Ok(BinaryOperator::NotEq), + Operator::LessThan => Ok(BinaryOperator::Lt), + Operator::GreaterThan => Ok(BinaryOperator::Gt), + Operator::LessThanOrEqual => Ok(BinaryOperator::LtEq), + Operator::GreaterThanOrEqual => Ok(BinaryOperator::GtEq), + Operator::RegexMatch => Ok(BinaryOperator::PGRegexMatch), + Operator::NotRegexMatch => Ok(BinaryOperator::PGRegexNotMatch), + Operator::Plus => Ok(BinaryOperator::Plus), + Operator::Minus => Ok(BinaryOperator::Minus), + Operator::Multiply => Ok(BinaryOperator::Multiply), + Operator::Divide => Ok(BinaryOperator::Divide), + Operator::Modulo => Ok(BinaryOperator::Modulo), + Operator::And => Ok(BinaryOperator::And), + Operator::Or => Ok(BinaryOperator::Or), + Operator::In + | Operator::NotIn + | Operator::Pow + | Operator::StartsWith + | Operator::EndsWith => Err(ShellError::UnsupportedOperator(operator, op)), + }?; + + let expr = Expr::BinaryOp { + left: Box::new(self.as_ref().clone()), + op: sql_operator, + right: Box::new(right_expr), + }; + + Ok(ExprDb(expr).into_value(lhs_span)) + } } impl ExprDb { - pub fn try_from_value(value: Value) -> Result { + 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, + *span, None, )), }, Value::String { val, .. } => Ok(Expr::Identifier(Ident { - value: val, + value: val.clone(), quote_style: None, }) .into()), + Value::Int { val, .. } => { + Ok(Expr::Value(sqlparser::ast::Value::Number(format!("{}", val), false)).into()) + } x => Err(ShellError::CantConvert( "database".into(), x.get_type().to_string(), @@ -82,17 +160,12 @@ impl ExprDb { } } - // 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_value(self, span: Span) -> Value { + Value::CustomValue { + val: Box::new(self), + span, + } + } pub fn into_native(self) -> Expr { self.0 @@ -101,6 +174,66 @@ impl ExprDb { pub fn to_value(&self, span: Span) -> Value { ExprDb::expr_to_value(self.as_ref(), span) } + + // Convenient function to extrac multiple Expr that could be inside a nushell Value + pub fn extract_exprs(value: Value) -> Result, ShellError> { + ExtractedExpr::extract_exprs(value).map(ExtractedExpr::into_exprs) + } +} + +enum ExtractedExpr { + Single(Expr), + List(Vec), +} + +impl ExtractedExpr { + fn into_exprs(self) -> Vec { + match self { + Self::Single(expr) => vec![expr], + Self::List(exprs) => exprs + .into_iter() + .flat_map(ExtractedExpr::into_exprs) + .collect(), + } + } + + fn extract_exprs(value: Value) -> Result { + match value { + Value::String { val, .. } => { + let expr = Expr::Identifier(Ident { + value: val, + quote_style: None, + }); + + Ok(ExtractedExpr::Single(expr)) + } + Value::Int { val, .. } => { + let expr = Expr::Value(sqlparser::ast::Value::Number(format!("{}", val), false)); + + Ok(ExtractedExpr::Single(expr)) + } + Value::Bool { val, .. } => { + let expr = Expr::Value(sqlparser::ast::Value::Boolean(val)); + + Ok(ExtractedExpr::Single(expr)) + } + Value::CustomValue { .. } => { + let expr = ExprDb::try_from_value(&value)?.into_native(); + Ok(ExtractedExpr::Single(expr)) + } + Value::List { vals, .. } => vals + .into_iter() + .map(Self::extract_exprs) + .collect::, ShellError>>() + .map(ExtractedExpr::List), + x => Err(ShellError::CantConvert( + "selection".into(), + x.get_type().to_string(), + x.span()?, + None, + )), + } + } } impl ExprDb { @@ -123,6 +256,24 @@ impl ExprDb { span, } } + Expr::Value(value) => Value::String { + val: format!("{}", value), + span, + }, + Expr::BinaryOp { left, op, right } => { + let cols = vec!["left".into(), "op".into(), "right".into()]; + let left = ExprDb::expr_to_value(left.as_ref(), span); + let right = ExprDb::expr_to_value(right.as_ref(), span); + let op = Value::String { + val: format!("{}", op), + span, + }; + + let vals = vec![left, op, right]; + + Value::Record { cols, vals, span } + } + Expr::Nested(expr) => ExprDb::expr_to_value(expr, span), Expr::CompoundIdentifier(_) => todo!(), Expr::IsNull(_) => todo!(), Expr::IsNotNull(_) => todo!(), @@ -132,7 +283,6 @@ impl ExprDb { Expr::InSubquery { .. } => todo!(), Expr::InUnnest { .. } => todo!(), Expr::Between { .. } => todo!(), - Expr::BinaryOp { .. } => todo!(), Expr::UnaryOp { .. } => todo!(), Expr::Cast { .. } => todo!(), Expr::TryCast { .. } => todo!(), @@ -140,8 +290,6 @@ impl ExprDb { Expr::Substring { .. } => todo!(), Expr::Trim { .. } => todo!(), Expr::Collate { .. } => todo!(), - Expr::Nested(_) => todo!(), - Expr::Value(_) => todo!(), Expr::TypedString { .. } => todo!(), Expr::MapAccess { .. } => todo!(), Expr::Function(_) => todo!(), diff --git a/crates/nu-command/src/database/values/dsl/select_item.rs b/crates/nu-command/src/database/values/dsl/select_item.rs index 8c4020c015..55ad7a5f06 100644 --- a/crates/nu-command/src/database/values/dsl/select_item.rs +++ b/crates/nu-command/src/database/values/dsl/select_item.rs @@ -1,7 +1,7 @@ use super::ExprDb; -use nu_protocol::{ast::PathMember, CustomValue, PipelineData, ShellError, Span, Value}; +use nu_protocol::{ast::PathMember, CustomValue, ShellError, Span, Value}; use serde::{Deserialize, Serialize}; -use sqlparser::ast::{Expr, Ident, SelectItem}; +use sqlparser::ast::{Expr, Ident, ObjectName, SelectItem}; #[derive(Debug, Serialize, Deserialize)] pub struct SelectDb(SelectItem); @@ -20,8 +20,14 @@ impl AsMut for SelectDb { } impl From for SelectDb { - fn from(expr: SelectItem) -> Self { - Self(expr) + fn from(selection: SelectItem) -> Self { + Self(selection) + } +} + +impl From for SelectDb { + fn from(expr: Expr) -> Self { + SelectItem::UnnamedExpr(expr).into() } } @@ -71,25 +77,52 @@ impl CustomValue for SelectDb { } impl SelectDb { - pub fn try_from_value(value: Value) -> Result { + 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(), + "db selection".into(), "non-expression".into(), - span, + *span, None, )), }, - Value::String { val, .. } => { - let expr = Expr::Identifier(Ident { - value: val, - quote_style: None, - }); + Value::String { val, .. } => match val.as_str() { + "*" => Ok(SelectItem::Wildcard.into()), + name if (name.contains('.') && name.contains('*')) => { + let parts: Vec = name + .split('.') + .filter(|part| part != &"*") + .map(|part| Ident { + value: part.to_string(), + quote_style: None, + }) + .collect(); - Ok(SelectItem::UnnamedExpr(expr).into()) - } + Ok(SelectItem::QualifiedWildcard(ObjectName(parts)).into()) + } + name if name.contains('.') => { + let parts: Vec = name + .split('.') + .map(|part| Ident { + value: part.to_string(), + quote_style: None, + }) + .collect(); + + let expr = Expr::CompoundIdentifier(parts); + Ok(SelectItem::UnnamedExpr(expr).into()) + } + _ => { + let expr = Expr::Identifier(Ident { + value: val.clone(), + quote_style: None, + }); + + Ok(SelectItem::UnnamedExpr(expr).into()) + } + }, x => Err(ShellError::CantConvert( "selection".into(), x.get_type().to_string(), @@ -99,11 +132,6 @@ impl SelectDb { } } - 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), @@ -203,16 +231,29 @@ impl ExtractedSelect { Ok(ExtractedSelect::Single(SelectItem::UnnamedExpr(expr))) } - Value::CustomValue { .. } => SelectDb::try_from_value(value) - .map(SelectDb::into_native) - .map(ExtractedSelect::Single), + Value::CustomValue { .. } => { + if let Ok(expr) = ExprDb::try_from_value(&value) { + Ok(ExtractedSelect::Single(SelectItem::UnnamedExpr( + expr.into_native(), + ))) + } else if let Ok(select) = SelectDb::try_from_value(&value) { + Ok(ExtractedSelect::Single(select.into_native())) + } else { + Err(ShellError::CantConvert( + "selection".into(), + value.get_type().to_string(), + value.span()?, + None, + )) + } + } Value::List { vals, .. } => vals .into_iter() .map(Self::extract_selects) .collect::, ShellError>>() .map(ExtractedSelect::List), x => Err(ShellError::CantConvert( - "expression".into(), + "selection".into(), x.get_type().to_string(), x.span()?, None,