diff --git a/crates/nu-command/src/database/commands/alias.rs b/crates/nu-command/src/database/commands/alias.rs index ca31ca48f7..9162f3625e 100644 --- a/crates/nu-command/src/database/commands/alias.rs +++ b/crates/nu-command/src/database/commands/alias.rs @@ -1,21 +1,19 @@ -use crate::{ - database::values::dsl::{ExprDb, SelectDb}, - SQLiteDatabase, -}; +use crate::SQLiteDatabase; use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, }; -use sqlparser::ast::{Ident, SelectItem, SetExpr, Statement, TableAlias, TableFactor}; +use sqlparser::ast::{Ident, SetExpr, Statement, TableAlias, TableFactor}; #[derive(Clone)] -pub struct AliasExpr; +pub struct AliasDb; -impl Command for AliasExpr { +impl Command for AliasDb { fn name(&self) -> &str { - "db as" + "as" } fn signature(&self) -> Signature { @@ -29,11 +27,67 @@ impl Command for AliasExpr { } fn examples(&self) -> Vec { - vec![Example { - description: "Creates an alias for a column selection", - example: "db col name_a | db as new_a", - result: None, - }] + vec![ + Example { + description: "Creates an alias for a selected table", + example: r#"open db.mysql + | into db + | select a + | from table_1 + | as t1 + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_1 AS t1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "Creates an alias for a derived table", + example: r#"open db.mysql + | into db + | select a + | from ( + open db.mysql + | into db + | select a b + | from table_a + ) + | as t1 + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM (SELECT a, b FROM table_a) AS t1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) } fn search_terms(&self) -> Vec<&str> { @@ -48,52 +102,12 @@ impl Command for AliasExpr { input: PipelineData, ) -> Result { let alias: String = call.req(engine_state, stack, 0)?; - let value = input.into_value(call.head); - 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, - )) - } + let db = SQLiteDatabase::try_from_pipeline(input, call.head)?; + alias_db(db, alias, call) } } -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, @@ -142,7 +156,7 @@ fn alias_db( }, s => { return Err(ShellError::GenericError( - "Connection doesnt define a query".into(), + "Connection doesn't define a query".into(), format!("Expected a connection with query. Got {}", s), Some(call.head), None, @@ -152,3 +166,19 @@ fn alias_db( }, } } + +#[cfg(test)] +mod test { + use super::super::{FromDb, ProjectionDb}; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(AliasDb {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + ]) + } +} diff --git a/crates/nu-command/src/database/commands/and.rs b/crates/nu-command/src/database/commands/and.rs index 8e4d6dc607..9c427350fa 100644 --- a/crates/nu-command/src/database/commands/and.rs +++ b/crates/nu-command/src/database/commands/and.rs @@ -6,7 +6,7 @@ use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, - Value, + Type, Value, }; use sqlparser::ast::{BinaryOperator, Expr, Query, Select, SetExpr, Statement}; @@ -15,11 +15,11 @@ pub struct AndDb; impl Command for AndDb { fn name(&self) -> &str { - "db and" + "and" } fn usage(&self) -> &str { - "Includes an AND clause for a query or expression" + "Includes an AND clause for a query" } fn signature(&self) -> Signature { @@ -32,26 +32,63 @@ impl Command for AndDb { vec!["database", "where"] } + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) + } + 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, + description: "Selects a column from a database with an AND clause", + example: r#"open db.mysql + | into db + | select a + | from table_1 + | where ((field a) > 1) + | and ((field b) == 1) + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_1 WHERE a > 1 AND b = 1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), }, 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, + description: "Creates a AND clause combined with an expression AND", + example: r#"open db.mysql + | into db + | select a + | from table_1 + | where ((field a) > 1 | and ((field a) < 10)) + | and ((field b) == 1) + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_1 WHERE (a > 1 AND a < 10) AND b = 1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), }, ] } @@ -66,51 +103,32 @@ impl Command for AndDb { 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()) { - match db.statement.as_mut() { - Some(statement) => match statement { - Statement::Query(query) => modify_query(query, expr, call.head)?, - s => { - return Err(ShellError::GenericError( - "Connection doesnt define a query".into(), - format!("Expected a connection with query. Got {}", s), - Some(call.head), - None, - Vec::new(), - )) - } - }, - None => { + let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?; + match db.statement.as_mut() { + Some(statement) => match statement { + Statement::Query(query) => modify_query(query, expr, call.head)?, + s => { return Err(ShellError::GenericError( - "Connection without statement".into(), - "The connection needs a statement defined".into(), + "Connection doesn't define a query".into(), + format!("Expected a connection with query. Got {}", s), Some(call.head), None, Vec::new(), )) } - }; + }, + None => { + return Err(ShellError::GenericError( + "Connection without statement".into(), + "The connection needs a statement defined".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, - )) - } + Ok(db.into_value(call.head).into_pipeline_data()) } } @@ -150,3 +168,23 @@ fn modify_select(select: &mut Box, expression: Expr, span: Span) -> Resu select.as_mut().selection = Some(new_expression); Ok(()) } + +#[cfg(test)] +mod test { + use super::super::super::expressions::{FieldExpr, OrExpr}; + use super::super::{FromDb, ProjectionDb, WhereDb}; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(OrDb {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + Box::new(WhereDb {}), + Box::new(FieldExpr {}), + Box::new(OrExpr {}), + ]) + } +} diff --git a/crates/nu-command/src/database/commands/order_by.rs b/crates/nu-command/src/database/commands/order_by.rs index 0afe69d062..3153198666 100644 --- a/crates/nu-command/src/database/commands/order_by.rs +++ b/crates/nu-command/src/database/commands/order_by.rs @@ -5,7 +5,8 @@ use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, }; use sqlparser::ast::{Expr, OrderByExpr, Statement}; @@ -14,7 +15,7 @@ pub struct OrderByDb; impl Command for OrderByDb { fn name(&self) -> &str { - "db order-by" + "order-by" } fn usage(&self) -> &str { @@ -34,19 +35,67 @@ impl Command for OrderByDb { } fn search_terms(&self) -> Vec<&str> { - vec!["database", "select"] + vec!["database", "order-by"] + } + + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) } fn examples(&self) -> Vec { - vec![Example { - description: "orders query by a column", - example: r#"db open db.mysql - | db from table_a - | db select a - | db order-by a - | db describe"#, - result: None, - }] + vec![ + Example { + description: "orders query by a column", + example: r#"open db.mysql + | into db + | from table_a + | select a + | order-by a + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_a ORDER BY a".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "orders query by column a ascending and by column b", + example: r#"open db.mysql + | into db + | from table_a + | select a + | order-by a --ascending + | order-by b + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_a ORDER BY a ASC, b".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] } fn run( @@ -155,3 +204,23 @@ fn update_connection( Ok(db.into_value(call.head).into_pipeline_data()) } + +#[cfg(test)] +mod test { + use super::super::super::expressions::{FieldExpr, OrExpr}; + use super::super::{FromDb, ProjectionDb, WhereDb}; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(OrderByDb {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + Box::new(WhereDb {}), + Box::new(FieldExpr {}), + Box::new(OrExpr {}), + ]) + } +} diff --git a/crates/nu-command/src/database/commands/over.rs b/crates/nu-command/src/database/commands/over.rs deleted file mode 100644 index b47f2b5db1..0000000000 --- a/crates/nu-command/src/database/commands/over.rs +++ /dev/null @@ -1,80 +0,0 @@ -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::{Expr, WindowSpec}; - -#[derive(Clone)] -pub struct OverExpr; - -impl Command for OverExpr { - fn name(&self) -> &str { - "db over" - } - - fn signature(&self) -> Signature { - Signature::build(self.name()) - .rest( - "partition-by", - SyntaxShape::Any, - "columns to partition the window function", - ) - .category(Category::Custom("database".into())) - } - - fn usage(&self) -> &str { - "Adds a partition to an expression function" - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Adds a partition to a function expresssion", - example: "db function avg col_a | db over col_b", - 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 vals: Vec = call.rest(engine_state, stack, 0)?; - let value = Value::List { - vals, - span: call.head, - }; - let partitions = ExprDb::extract_exprs(value)?; - - let mut expression = ExprDb::try_from_pipeline(input, call.head)?; - match expression.as_mut() { - Expr::Function(function) => { - function.over = Some(WindowSpec { - partition_by: partitions, - order_by: Vec::new(), - window_frame: None, - }); - } - s => { - return Err(ShellError::GenericError( - "Expression doesnt define a function".into(), - format!("Expected an expression with a function. Got {}", s), - Some(call.head), - None, - Vec::new(), - )) - } - }; - - Ok(expression.into_value(call.head).into_pipeline_data()) - } -} diff --git a/crates/nu-command/src/database/commands/query.rs b/crates/nu-command/src/database/commands/query.rs index 63c76d175a..cc0dae4177 100644 --- a/crates/nu-command/src/database/commands/query.rs +++ b/crates/nu-command/src/database/commands/query.rs @@ -3,6 +3,7 @@ use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape, + Type, }; use super::super::SQLiteDatabase; @@ -12,7 +13,7 @@ pub struct QueryDb; impl Command for QueryDb { fn name(&self) -> &str { - "db query" + "query" } fn signature(&self) -> Signature { @@ -29,10 +30,18 @@ impl Command for QueryDb { "Query a database using SQL." } + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Any + } + fn examples(&self) -> Vec { vec![Example { - description: "Get 1 table out of a SQLite database", - example: r#"db open foo.db | db query "SELECT * FROM Bar""#, + description: "Execute a query statement using the database connection", + example: r#"open foo.db | into db | query "SELECT * FROM Bar""#, result: None, }] } diff --git a/crates/nu-command/src/database/commands/schema.rs b/crates/nu-command/src/database/commands/schema.rs index b8261b7eaf..a53b5b584a 100644 --- a/crates/nu-command/src/database/commands/schema.rs +++ b/crates/nu-command/src/database/commands/schema.rs @@ -3,7 +3,7 @@ use crate::database::values::definitions::{db::Db, db_row::DbRow, db_table::DbTa use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Span, Value, + Category, Example, PipelineData, ShellError, Signature, Span, Type, Value, }; use rusqlite::Connection; #[derive(Clone)] @@ -11,7 +11,7 @@ pub struct SchemaDb; impl Command for SchemaDb { fn name(&self) -> &str { - "db schema" + "schema" } fn signature(&self) -> Signature { @@ -19,13 +19,21 @@ impl Command for SchemaDb { } fn usage(&self) -> &str { - "Show database information, including its schema." + "Show sqlite database information, including its schema." + } + + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Any } fn examples(&self) -> Vec { vec![Example { description: "Show the schema of a SQLite database", - example: r#"open foo.db | db schema"#, + example: r#"open foo.db | into db | schema"#, result: None, }] } diff --git a/crates/nu-command/src/database/commands/select.rs b/crates/nu-command/src/database/commands/select.rs index e550b510b9..c3ff73dd76 100644 --- a/crates/nu-command/src/database/commands/select.rs +++ b/crates/nu-command/src/database/commands/select.rs @@ -4,7 +4,7 @@ use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, - Value, + Type, Value, }; use sqlparser::ast::{Query, Select, SelectItem, SetExpr, Statement}; @@ -13,7 +13,7 @@ pub struct ProjectionDb; impl Command for ProjectionDb { fn name(&self) -> &str { - "db select" + "select" } fn usage(&self) -> &str { @@ -34,17 +34,55 @@ impl Command for ProjectionDb { vec!["database", "select"] } + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) + } + fn examples(&self) -> Vec { vec![ Example { description: "selects a column from a database", - example: "db open db.mysql | db select a | db describe", - result: None, + example: "open db.mysql | into db | select a | describe", + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), }, Example { - description: "selects columns from a database", - example: "db open db.mysql | db select a b c | db describe", - result: None, + description: "selects columns from a database using alias", + example: r#"open db.mysql + | into db + | select (field a | as new_a) b c + | from table_1 + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a AS new_a, b, c FROM table_1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), }, ] } @@ -104,7 +142,7 @@ fn modify_statement( Ok(statement) } s => Err(ShellError::GenericError( - "Connection doesnt define a statement".into(), + "Connection doesn't define a statement".into(), format!("Expected a connection with query. Got {}", s), Some(span), None, @@ -129,3 +167,21 @@ fn create_select(projection: Vec) -> Select { having: None, } } + +#[cfg(test)] +mod test { + use super::super::super::expressions::{AliasExpr, FieldExpr}; + use super::super::FromDb; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + Box::new(FieldExpr {}), + Box::new(AliasExpr {}), + ]) + } +} diff --git a/crates/nu-command/src/database/commands/testing.rs b/crates/nu-command/src/database/commands/testing.rs index 84451cf721..4fd286e299 100644 --- a/crates/nu-command/src/database/commands/testing.rs +++ b/crates/nu-command/src/database/commands/testing.rs @@ -13,7 +13,7 @@ pub struct TestingDb; impl Command for TestingDb { fn name(&self) -> &str { - "db testing" + "testing-db" } fn signature(&self) -> Signature { @@ -27,7 +27,7 @@ impl Command for TestingDb { } fn usage(&self) -> &str { - "Create query object" + "Temporal Command: Create query object" } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/database/commands/to_db.rs b/crates/nu-command/src/database/commands/to_db.rs new file mode 100644 index 0000000000..462669cc39 --- /dev/null +++ b/crates/nu-command/src/database/commands/to_db.rs @@ -0,0 +1,55 @@ +use super::super::SQLiteDatabase; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, +}; + +#[derive(Clone)] +pub struct ToDataBase; + +impl Command for ToDataBase { + fn name(&self) -> &str { + "into db" + } + + fn usage(&self) -> &str { + "Converts into an open db connection" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("database".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "into", "db"] + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Converts an open file into a db object", + example: "open db.mysql | into db", + result: None, + }] + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let db = SQLiteDatabase::try_from_pipeline(input, call.head)?; + Ok(db.into_value(call.head).into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/database/commands/where_.rs b/crates/nu-command/src/database/commands/where_.rs index f14a22afa0..17ef0509d9 100644 --- a/crates/nu-command/src/database/commands/where_.rs +++ b/crates/nu-command/src/database/commands/where_.rs @@ -5,7 +5,8 @@ use nu_engine::CallExt; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, + Type, Value, }; use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement}; @@ -14,7 +15,7 @@ pub struct WhereDb; impl Command for WhereDb { fn name(&self) -> &str { - "db where" + "where" } fn usage(&self) -> &str { @@ -31,15 +32,37 @@ impl Command for WhereDb { vec!["database", "where"] } + fn input_type(&self) -> Type { + Type::Custom("database".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) + } + 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, + example: r#"open db.mysql + | into db + | select a + | from table_1 + | where ((field a) > 1) + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT a FROM table_1 WHERE a > 1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), }] } @@ -111,3 +134,23 @@ fn create_select(expression: Expr) -> Select { having: None, } } + +#[cfg(test)] +mod test { + use super::super::super::expressions::{FieldExpr, OrExpr}; + use super::super::{FromDb, ProjectionDb}; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(WhereDb {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + Box::new(WhereDb {}), + Box::new(FieldExpr {}), + Box::new(OrExpr {}), + ]) + } +} 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..260d039eed --- /dev/null +++ b/crates/nu-command/src/database/expressions/alias.rs @@ -0,0 +1,138 @@ +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, Span, SyntaxShape, + Type, Value, +}; +use sqlparser::ast::{Ident, SelectItem}; + +#[derive(Clone)] +pub struct AliasExpr; + +impl Command for AliasExpr { + fn name(&self) -> &str { + "as" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("alias", SyntaxShape::String, "alias name") + .category(Category::Custom("db-expression".into())) + } + + fn usage(&self) -> &str { + "Creates an alias for a column selection" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates an alias for a column selection", + example: "field name_a | as new_a | into nu", + result: Some(Value::Record { + cols: vec!["expression".into(), "alias".into()], + vals: vec![ + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "name_a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "new_a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + fn input_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "alias", "column"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let alias: String = call.req(engine_state, stack, 0)?; + + let value = input.into_value(call.head); + if let Ok(expr) = ExprDb::try_from_value(&value) { + alias_selection(expr.into_native().into(), alias, call) + } else { + let select = SelectDb::try_from_value(&value)?; + alias_selection(select, alias, call) + } + } +} + +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()) +} + +#[cfg(test)] +mod test { + use super::super::FieldExpr; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![Box::new(AliasExpr {}), Box::new(FieldExpr {})]) + } +} diff --git a/crates/nu-command/src/database/expressions/and.rs b/crates/nu-command/src/database/expressions/and.rs new file mode 100644 index 0000000000..5ec0f532ea --- /dev/null +++ b/crates/nu-command/src/database/expressions/and.rs @@ -0,0 +1,147 @@ +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, Span, SyntaxShape, + Type, Value, +}; +use sqlparser::ast::{BinaryOperator, Expr}; + +#[derive(Clone)] +pub struct AndExpr; + +impl Command for AndExpr { + fn name(&self) -> &str { + "and" + } + + fn usage(&self) -> &str { + "Includes an AND clause for an expression" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("and", SyntaxShape::Any, "AND expression") + .category(Category::Custom("db-expression".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "and", "expression"] + } + + fn input_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates an AND expression", + example: r#"(field a) > 1 | and ((field a) < 10) | into nu"#, + result: Some(Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: ">".into(), + span: Span::test_data(), + }, + Value::String { + val: "1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: "AND".into(), + span: Span::test_data(), + }, + Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: "<".into(), + span: Span::test_data(), + }, + Value::String { + val: "10".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + 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 expression = ExprDb::try_from_pipeline(input, call.head)?; + 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()) + } +} + +#[cfg(test)] +mod test { + use super::super::FieldExpr; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![Box::new(AndExpr {}), Box::new(FieldExpr {})]) + } +} diff --git a/crates/nu-command/src/database/expressions/as_nu.rs b/crates/nu-command/src/database/expressions/as_nu.rs new file mode 100644 index 0000000000..e5ebaa34c9 --- /dev/null +++ b/crates/nu-command/src/database/expressions/as_nu.rs @@ -0,0 +1,81 @@ +use crate::database::values::dsl::{ExprDb, SelectDb}; + +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, +}; + +#[derive(Clone)] +pub struct ExprAsNu; + +impl Command for ExprAsNu { + fn name(&self) -> &str { + "into nu" + } + + fn usage(&self) -> &str { + "Convert a db expression into a nu value for access and exploration" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Custom("db-expression".into())) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Convert a col expression into a nushell value", + example: "field name_1 | into nu", + result: Some(Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "name_1".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + fn input_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn output_type(&self) -> Type { + Type::Any + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let value = input.into_value(call.head); + if let Ok(expr) = ExprDb::try_from_value(&value) { + Ok(expr.to_value(call.head).into_pipeline_data()) + } else { + let select = SelectDb::try_from_value(&value)?; + Ok(select.to_value(call.head).into_pipeline_data()) + } + } +} + +#[cfg(test)] +mod test { + use super::super::FieldExpr; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![Box::new(ExprAsNu {}), Box::new(FieldExpr {})]) + } +} diff --git a/crates/nu-command/src/database/expressions/field.rs b/crates/nu-command/src/database/expressions/field.rs new file mode 100644 index 0000000000..33299763e5 --- /dev/null +++ b/crates/nu-command/src/database/expressions/field.rs @@ -0,0 +1,84 @@ +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, Span, SyntaxShape, + Type, Value, +}; + +#[derive(Clone)] +pub struct FieldExpr; + +impl Command for FieldExpr { + fn name(&self) -> &str { + "field" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("name", SyntaxShape::String, "column name") + .category(Category::Custom("db-expression".into())) + } + + fn usage(&self) -> &str { + "Creates column expression for database" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "column", "expression"] + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates a named field expression", + example: "field name_1 | into nu", + result: Some(Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "name_1".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + let expression = ExprDb::try_from_value(&value)?; + + Ok(expression.into_value(call.head).into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![Box::new(FieldExpr {})]) + } +} diff --git a/crates/nu-command/src/database/expressions/function.rs b/crates/nu-command/src/database/expressions/function.rs new file mode 100644 index 0000000000..642e52e096 --- /dev/null +++ b/crates/nu-command/src/database/expressions/function.rs @@ -0,0 +1,163 @@ +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, Span, SyntaxShape, + Type, Value, +}; +use sqlparser::ast::{Expr, Function, FunctionArg, FunctionArgExpr, Ident, ObjectName}; + +#[derive(Clone)] +pub struct FunctionExpr; + +impl Command for FunctionExpr { + fn name(&self) -> &str { + "fn" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("name", SyntaxShape::String, "function name") + .switch("distinct", "distict values", Some('d')) + .rest("arguments", SyntaxShape::Any, "function arguments") + .category(Category::Custom("db-expression".into())) + } + + fn usage(&self) -> &str { + "Creates function expression for a select operation" + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Creates a function expression", + example: "fn count name_1 | into nu", + result: Some(Value::Record { + cols: vec![ + "name".into(), + "args".into(), + "over".into(), + "distinct".into(), + ], + vals: vec![ + Value::String { + val: "count".into(), + span: Span::test_data(), + }, + Value::List { + vals: vec![Value::String { + val: "name_1".into(), + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + Value::Bool { + val: false, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "orders query by a column", + example: r#"open db.mysql + | into db + | select (fn lead col_a) + | from table_a + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT lead(col_a) FROM table_a".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "function", "expression"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let name: String = call.req(engine_state, stack, 0)?; + let vals: Vec = call.rest(engine_state, stack, 1)?; + let value = Value::List { + vals, + span: call.head, + }; + let expressions = ExprDb::extract_exprs(value)?; + + let name: Vec = name + .split('.') + .map(|part| Ident { + value: part.to_string(), + quote_style: None, + }) + .collect(); + let name = ObjectName(name); + + let args: Vec = expressions + .into_iter() + .map(|expr| { + let arg = FunctionArgExpr::Expr(expr); + + FunctionArg::Unnamed(arg) + }) + .collect(); + + let expression: ExprDb = Expr::Function(Function { + name, + args, + over: None, + distinct: call.has_flag("distinct"), + }) + .into(); + + Ok(expression.into_value(call.head).into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::super::super::commands::{FromDb, ProjectionDb}; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(FunctionExpr {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + ]) + } +} 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..5f182864bf --- /dev/null +++ b/crates/nu-command/src/database/expressions/mod.rs @@ -0,0 +1,40 @@ +// Conversions between value and sqlparser objects +mod alias; +mod and; +mod as_nu; +mod field; +mod function; +mod or; +mod over; + +use nu_protocol::engine::StateWorkingSet; + +pub(crate) use alias::AliasExpr; +pub(crate) use and::AndExpr; +pub(crate) use as_nu::ExprAsNu; +pub(crate) use field::FieldExpr; +pub(crate) use function::FunctionExpr; +pub(crate) use or::OrExpr; +pub(crate) use over::OverExpr; + +pub fn add_expressions_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!( + ExprAsNu, + AliasExpr, + AndExpr, + FieldExpr, + FunctionExpr, + OrExpr, + OverExpr + ); +} diff --git a/crates/nu-command/src/database/expressions/or.rs b/crates/nu-command/src/database/expressions/or.rs new file mode 100644 index 0000000000..71180e5535 --- /dev/null +++ b/crates/nu-command/src/database/expressions/or.rs @@ -0,0 +1,147 @@ +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, Span, SyntaxShape, + Type, Value, +}; +use sqlparser::ast::{BinaryOperator, Expr}; + +#[derive(Clone)] +pub struct OrExpr; + +impl Command for OrExpr { + fn name(&self) -> &str { + "or" + } + + fn usage(&self) -> &str { + "Includes an OR clause for an expression" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("or", SyntaxShape::Any, "OR expression") + .category(Category::Custom("db-expression".into())) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "or", "expression"] + } + + fn input_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Creates an AND expression", + example: r#"(field a) > 1 | or ((field a) < 10) | into nu"#, + result: Some(Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: ">".into(), + span: Span::test_data(), + }, + Value::String { + val: "1".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: "OR".into(), + span: Span::test_data(), + }, + Value::Record { + cols: vec!["left".into(), "op".into(), "right".into()], + vals: vec![ + Value::Record { + cols: vec!["value".into(), "quoted_style".into()], + vals: vec![ + Value::String { + val: "a".into(), + span: Span::test_data(), + }, + Value::String { + val: "None".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + Value::String { + val: "<".into(), + span: Span::test_data(), + }, + Value::String { + val: "10".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }] + } + + 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 expression = ExprDb::try_from_pipeline(input, call.head)?; + 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()) + } +} + +#[cfg(test)] +mod test { + use super::super::FieldExpr; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![Box::new(OrExpr {}), Box::new(FieldExpr {})]) + } +} diff --git a/crates/nu-command/src/database/expressions/over.rs b/crates/nu-command/src/database/expressions/over.rs new file mode 100644 index 0000000000..409320c22a --- /dev/null +++ b/crates/nu-command/src/database/expressions/over.rs @@ -0,0 +1,159 @@ +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, Span, SyntaxShape, + Type, Value, +}; +use sqlparser::ast::{Expr, WindowSpec}; + +#[derive(Clone)] +pub struct OverExpr; + +impl Command for OverExpr { + fn name(&self) -> &str { + "over" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .rest( + "partition-by", + SyntaxShape::Any, + "columns to partition the window function", + ) + .category(Category::Custom("db-expression".into())) + } + + fn usage(&self) -> &str { + "Adds a partition to an expression function" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Adds a partition to a function expression", + example: "fn avg col_a | over col_b | into nu", + result: Some(Value::Record { + cols: vec![ + "name".into(), + "args".into(), + "over".into(), + "distinct".into(), + ], + vals: vec![ + Value::String { + val: "avg".into(), + span: Span::test_data(), + }, + Value::List { + vals: vec![Value::String { + val: "col_a".into(), + span: Span::test_data(), + }], + span: Span::test_data(), + }, + Value::String { + val: "Some(WindowSpec { partition_by: [Identifier(Ident { value: \"col_b\", quote_style: None })], order_by: [], window_frame: None })".into(), + span: Span::test_data(), + }, + Value::Bool { + val: false, + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + Example { + description: "orders query by a column", + example: r#"open db.mysql + | into db + | select (fn lead col_a | over col_b) + | from table_a + | describe"#, + result: Some(Value::Record { + cols: vec!["connection".into(), "query".into()], + vals: vec![ + Value::String { + val: "db.mysql".into(), + span: Span::test_data(), + }, + Value::String { + val: "SELECT lead(col_a) OVER (PARTITION BY col_b) FROM table_a".into(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn input_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn output_type(&self) -> Type { + Type::Custom("db-expression".into()) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "over", "expression"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let vals: Vec = call.rest(engine_state, stack, 0)?; + let value = Value::List { + vals, + span: call.head, + }; + let partitions = ExprDb::extract_exprs(value)?; + + let mut expression = ExprDb::try_from_pipeline(input, call.head)?; + match expression.as_mut() { + Expr::Function(function) => { + function.over = Some(WindowSpec { + partition_by: partitions, + order_by: Vec::new(), + window_frame: None, + }); + } + s => { + return Err(ShellError::GenericError( + "Expression doesnt define a function".into(), + format!("Expected an expression with a function. Got {}", s), + Some(call.head), + None, + Vec::new(), + )) + } + }; + + Ok(expression.into_value(call.head).into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::super::super::commands::{FromDb, ProjectionDb}; + use super::super::FunctionExpr; + use super::*; + use crate::database::test_database::test_database; + + #[test] + fn test_examples() { + test_database(vec![ + Box::new(OverExpr {}), + Box::new(FunctionExpr {}), + Box::new(ProjectionDb {}), + Box::new(FromDb {}), + ]) + } +} diff --git a/crates/nu-command/src/database/mod.rs b/crates/nu-command/src/database/mod.rs index b2393f0591..dc0503c74e 100644 --- a/crates/nu-command/src/database/mod.rs +++ b/crates/nu-command/src/database/mod.rs @@ -1,8 +1,21 @@ mod commands; +mod expressions; mod values; -pub use commands::add_database_decls; +use commands::add_commands_decls; +use expressions::add_expressions_decls; + pub use values::{ convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_connection_in_memory, SQLiteDatabase, }; + +use nu_protocol::engine::StateWorkingSet; + +pub fn add_database_decls(working_set: &mut StateWorkingSet) { + add_commands_decls(working_set); + add_expressions_decls(working_set); +} + +#[cfg(test)] +mod test_database; diff --git a/crates/nu-command/src/database/test_database.rs b/crates/nu-command/src/database/test_database.rs new file mode 100644 index 0000000000..83d8c63164 --- /dev/null +++ b/crates/nu-command/src/database/test_database.rs @@ -0,0 +1,144 @@ +use std::path::Path; + +use super::commands::{DescribeDb, ToDataBase}; +use super::expressions::ExprAsNu; +use crate::SQLiteDatabase; +use nu_engine::{eval_block, CallExt}; +use nu_parser::parse; +use nu_protocol::{ + engine::{Command, EngineState, Stack, StateWorkingSet}, + Category, IntoPipelineData, PipelineData, Signature, Span, Type, +}; + +#[derive(Clone)] +pub struct CustomOpen; + +impl Command for CustomOpen { + fn name(&self) -> &str { + "open" + } + + fn usage(&self) -> &str { + "Mock open file command" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build(self.name()) + .required( + "filename", + nu_protocol::SyntaxShape::String, + "the filename to use", + ) + .category(Category::Custom("database".into())) + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &nu_protocol::ast::Call, + _input: nu_protocol::PipelineData, + ) -> Result { + let path: String = call.req(engine_state, stack, 0)?; + let path = Path::new(&path); + + let db = SQLiteDatabase::new(path); + Ok(db.into_value(call.head).into_pipeline_data()) + } + + fn input_type(&self) -> Type { + Type::Any + } + + fn output_type(&self) -> Type { + Type::Custom("database".into()) + } +} + +pub fn test_database(cmds: Vec>) { + if cmds.is_empty() { + panic!("Empty commands vector") + } + + // The first element in the cmds vector must be the one tested + let examples = cmds[0].examples(); + let mut engine_state = Box::new(EngineState::new()); + + let delta = { + // Base functions that are needed for testing + // Try to keep this working set small to keep tests running as fast as possible + let mut working_set = StateWorkingSet::new(&*engine_state); + working_set.add_decl(Box::new(DescribeDb {})); + working_set.add_decl(Box::new(ToDataBase {})); + working_set.add_decl(Box::new(CustomOpen {})); + working_set.add_decl(Box::new(ExprAsNu {})); + + // Adding the command that is being tested to the working set + for cmd in cmds { + working_set.add_decl(cmd); + } + + working_set.render() + }; + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let _ = engine_state.merge_delta(delta, None, &cwd); + + for example in examples { + // Skip tests that don't have results to compare to + if example.result.is_none() { + continue; + } + let start = std::time::Instant::now(); + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(&*engine_state); + let (output, err) = parse( + &mut working_set, + None, + example.example.as_bytes(), + false, + &[], + ); + + if let Some(err) = err { + panic!("test parse error in `{}`: {:?}", example.example, err) + } + + (output, working_set.render()) + }; + + let _ = engine_state.merge_delta(delta, None, &cwd); + + let mut stack = Stack::new(); + + match eval_block( + &engine_state, + &mut stack, + &block, + PipelineData::new(Span::test_data()), + true, + true, + ) { + Err(err) => panic!("test eval error in `{}`: {:?}", example.example, err), + Ok(result) => { + let result = result.into_value(Span::test_data()); + println!("input: {}", example.example); + println!("result: {:?}", result); + println!("done: {:?}", start.elapsed()); + + // Note. Value implements PartialEq for Bool, Int, Float, String and Block + // If the command you are testing requires to compare another case, then + // you need to define its equality in the Value struct + if let Some(expected) = example.result { + if result != expected { + panic!( + "the example result is different to expected value: {:?} != {:?}", + result, expected + ) + } + } + } + } + } +} diff --git a/crates/nu-command/tests/commands/query/db.rs b/crates/nu-command/tests/commands/query/db.rs index d9eea3f12e..b84b5cb8f8 100644 --- a/crates/nu-command/tests/commands/query/db.rs +++ b/crates/nu-command/tests/commands/query/db.rs @@ -1,12 +1,14 @@ use nu_test_support::{nu, pipeline}; +#[cfg(feature = "database")] #[test] fn can_query_single_table() { let actual = nu!( cwd: "tests/fixtures/formats", pipeline( r#" open sample.db - | db query "select * from strings" + | into db + | query "select * from strings" | where x =~ ell | length "# @@ -15,25 +17,28 @@ fn can_query_single_table() { assert_eq!(actual.out, "4"); } +#[cfg(feature = "database")] #[test] fn invalid_sql_fails() { let actual = nu!( cwd: "tests/fixtures/formats", pipeline( r#" open sample.db - | db query "select *asdfasdf" + | into db + | query "select *asdfasdf" "# )); assert!(actual.err.contains("syntax error")); } +#[cfg(feature = "database")] #[test] fn invalid_input_fails() { let actual = nu!( cwd: "tests/fixtures/formats", pipeline( r#" - "foo" | db query "select * from asdf" + "foo" | into db | query "select * from asdf" "# ));