join and from derived tables (#5477)

This commit is contained in:
Fernando Herrera
2022-05-08 11:12:03 +01:00
committed by GitHub
parent 374757f286
commit 061e9294b3
8 changed files with 372 additions and 69 deletions

View File

@@ -29,26 +29,15 @@ impl Command for AliasExpr {
}
fn examples(&self) -> Vec<Example> {
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,
},
]
vec![Example {
description: "Creates an alias for a column selection",
example: "db col name_a | db as new_a",
result: None,
}]
}
fn search_terms(&self) -> Vec<&str> {
vec!["database", "column", "expression"]
vec!["database", "alias", "column"]
}
fn run(

View File

@@ -0,0 +1,66 @@
use crate::{database::values::definitions::ConnectionDb, SQLiteDatabase};
use nu_protocol::{ShellError, Value};
use sqlparser::ast::{ObjectName, Statement, TableAlias, TableFactor};
pub fn value_into_table_factor(
table: Value,
connection: &ConnectionDb,
alias: Option<TableAlias>,
) -> Result<TableFactor, ShellError> {
match table {
Value::String { val, .. } => {
let ident = sqlparser::ast::Ident {
value: val,
quote_style: None,
};
Ok(TableFactor::Table {
name: ObjectName(vec![ident]),
alias,
args: Vec::new(),
with_hints: Vec::new(),
})
}
Value::CustomValue { span, .. } => {
let db = SQLiteDatabase::try_from_value(table)?;
if &db.connection != connection {
return Err(ShellError::GenericError(
"Incompatible connections".into(),
"trying to join on table with different connection".into(),
Some(span),
None,
Vec::new(),
));
}
match db.statement {
Some(statement) => match statement {
Statement::Query(query) => Ok(TableFactor::Derived {
lateral: false,
subquery: query,
alias,
}),
s => Err(ShellError::GenericError(
"Connection doesnt define a query".into(),
format!("Expected a connection with query. Got {}", s),
Some(span),
None,
Vec::new(),
)),
},
None => Err(ShellError::GenericError(
"Error creating derived table".into(),
"there is no statement defined yet".into(),
Some(span),
None,
Vec::new(),
)),
}
}
_ => Err(ShellError::UnsupportedInput(
"String or connection".into(),
table.span()?,
)),
}
}

View File

@@ -1,13 +1,13 @@
use super::super::SQLiteDatabase;
use crate::database::values::definitions::ConnectionDb;
use super::{super::SQLiteDatabase, conversions::value_into_table_factor};
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape,
};
use sqlparser::ast::{
Ident, ObjectName, Query, Select, SetExpr, Statement, TableFactor, TableWithJoins,
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
};
use sqlparser::ast::{Ident, Query, Select, SetExpr, Statement, TableAlias, TableWithJoins};
#[derive(Clone)]
pub struct FromDb;
@@ -25,8 +25,14 @@ impl Command for FromDb {
Signature::build(self.name())
.required(
"select",
SyntaxShape::Any,
"table of derived table to select from",
)
.named(
"as",
SyntaxShape::String,
"Name of table to select from",
"Alias for the selected table",
Some('a'),
)
.category(Category::Custom("database".into()))
}
@@ -50,22 +56,36 @@ impl Command for FromDb {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let table: String = call.req(engine_state, stack, 0)?;
let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
db.statement = match db.statement {
None => Some(create_statement(table)),
Some(statement) => Some(modify_statement(statement, table, call.head)?),
None => Some(create_statement(&db.connection, engine_state, stack, call)?),
Some(statement) => Some(modify_statement(
&db.connection,
statement,
engine_state,
stack,
call,
)?),
};
Ok(db.into_value(call.head).into_pipeline_data())
}
}
fn create_statement(table: String) -> Statement {
fn create_statement(
connection: &ConnectionDb,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<Statement, ShellError> {
let query = Query {
with: None,
body: SetExpr::Select(Box::new(create_select(table))),
body: SetExpr::Select(Box::new(create_select(
connection,
engine_state,
stack,
call,
)?)),
order_by: Vec::new(),
limit: None,
offset: None,
@@ -73,42 +93,57 @@ fn create_statement(table: String) -> Statement {
lock: None,
};
Statement::Query(Box::new(query))
Ok(Statement::Query(Box::new(query)))
}
fn modify_statement(
connection: &ConnectionDb,
mut statement: Statement,
table: String,
span: Span,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<Statement, ShellError> {
match statement {
Statement::Query(ref mut query) => {
match query.body {
SetExpr::Select(ref mut select) => select.as_mut().from = create_from(table),
SetExpr::Select(ref mut select) => {
let table = create_table(connection, engine_state, stack, call)?;
select.from.push(table);
}
_ => {
query.as_mut().body = SetExpr::Select(Box::new(create_select(table)));
query.as_mut().body = SetExpr::Select(Box::new(create_select(
connection,
engine_state,
stack,
call,
)?));
}
};
Ok(statement)
}
s => Err(ShellError::GenericError(
"Connection doesnt define a statement".into(),
"Connection doesnt define a query".into(),
format!("Expected a connection with query. Got {}", s),
Some(span),
Some(call.head),
None,
Vec::new(),
)),
}
}
fn create_select(table: String) -> Select {
Select {
fn create_select(
connection: &ConnectionDb,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<Select, ShellError> {
Ok(Select {
distinct: false,
top: None,
projection: Vec::new(),
into: None,
from: create_from(table),
from: vec![create_table(connection, engine_state, stack, call)?],
lateral_views: Vec::new(),
selection: None,
group_by: Vec::new(),
@@ -116,29 +151,32 @@ fn create_select(table: String) -> Select {
distribute_by: Vec::new(),
sort_by: Vec::new(),
having: None,
}
})
}
// This function needs more work
// It needs to define multi tables and joins
// I assume we will need to define expressions for the columns instead of strings
fn create_from(table: String) -> Vec<TableWithJoins> {
let ident = Ident {
value: table,
quote_style: None,
};
fn create_table(
connection: &ConnectionDb,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<TableWithJoins, ShellError> {
let alias = call
.get_flag::<String>(engine_state, stack, "as")?
.map(|alias| TableAlias {
name: Ident {
value: alias,
quote_style: None,
},
columns: Vec::new(),
});
let table_factor = TableFactor::Table {
name: ObjectName(vec![ident]),
alias: None,
args: Vec::new(),
with_hints: Vec::new(),
};
let select_table: Value = call.req(engine_state, stack, 0)?;
let table_factor = value_into_table_factor(select_table, connection, alias)?;
let table = TableWithJoins {
relation: table_factor,
joins: Vec::new(),
};
vec![table]
Ok(table)
}

View File

@@ -0,0 +1,178 @@
use super::{super::SQLiteDatabase, conversions::value_into_table_factor};
use crate::database::values::{definitions::ConnectionDb, 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, Join, JoinConstraint, JoinOperator, Select, SetExpr, Statement, TableAlias,
};
#[derive(Clone)]
pub struct JoinDb;
impl Command for JoinDb {
fn name(&self) -> &str {
"db join"
}
fn usage(&self) -> &str {
"Joins with another table or derived table. Default join type is inner"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required(
"table",
SyntaxShape::Any,
"table or derived table to join on",
)
.required("on", SyntaxShape::Any, "expression to join tables")
.named(
"as",
SyntaxShape::String,
"Alias for the selected join",
Some('a'),
)
.switch("left", "left outer join", Some('l'))
.switch("right", "right outer join", Some('r'))
.switch("outer", "full outer join", Some('o'))
.switch("cross", "cross join", Some('c'))
.category(Category::Custom("database".into()))
}
fn search_terms(&self) -> Vec<&str> {
vec!["database", "join"]
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "",
example: "",
result: None,
}]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
db.statement = match db.statement {
Some(statement) => Some(modify_statement(
&db.connection,
statement,
engine_state,
stack,
call,
)?),
None => {
return Err(ShellError::GenericError(
"Error creating join".into(),
"there is no statement defined yet".into(),
Some(call.head),
None,
Vec::new(),
))
}
};
Ok(db.into_value(call.head).into_pipeline_data())
}
}
fn modify_statement(
connection: &ConnectionDb,
mut statement: Statement,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<Statement, ShellError> {
match statement {
Statement::Query(ref mut query) => {
match &mut query.body {
SetExpr::Select(ref mut select) => {
modify_from(connection, select, engine_state, stack, call)?
}
s => {
return Err(ShellError::GenericError(
"Connection doesnt define a select".into(),
format!("Expected a connection with select. Got {}", s),
Some(call.head),
None,
Vec::new(),
))
}
};
Ok(statement)
}
s => Err(ShellError::GenericError(
"Connection doesnt define a query".into(),
format!("Expected a connection with query. Got {}", s),
Some(call.head),
None,
Vec::new(),
)),
}
}
fn modify_from(
connection: &ConnectionDb,
select: &mut Select,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<(), ShellError> {
match select.from.last_mut() {
Some(table) => {
let alias = call
.get_flag::<String>(engine_state, stack, "as")?
.map(|alias| TableAlias {
name: Ident {
value: alias,
quote_style: None,
},
columns: Vec::new(),
});
let join_table: Value = call.req(engine_state, stack, 0)?;
let table_factor = value_into_table_factor(join_table, connection, alias)?;
let on_expr: Value = call.req(engine_state, stack, 1)?;
let on_expr = ExprDb::try_from_value(&on_expr)?;
let join_on = if call.has_flag("left") {
JoinOperator::LeftOuter(JoinConstraint::On(on_expr.into_native()))
} else if call.has_flag("right") {
JoinOperator::RightOuter(JoinConstraint::On(on_expr.into_native()))
} else if call.has_flag("outer") {
JoinOperator::FullOuter(JoinConstraint::On(on_expr.into_native()))
} else {
JoinOperator::Inner(JoinConstraint::On(on_expr.into_native()))
};
let join = Join {
relation: table_factor,
join_operator: join_on,
};
table.joins.push(join);
Ok(())
}
None => Err(ShellError::GenericError(
"Connection without table defined".into(),
"Expected a table defined".into(),
Some(call.head),
None,
Vec::new(),
)),
}
}

View File

@@ -1,3 +1,6 @@
// Conversions between value and sqlparser objects
pub mod conversions;
mod alias;
mod and;
mod col;
@@ -7,6 +10,7 @@ mod describe;
mod from;
mod function;
mod group_by;
mod join;
mod limit;
mod open;
mod or;
@@ -32,6 +36,7 @@ use describe::DescribeDb;
use from::FromDb;
use function::FunctionExpr;
use group_by::GroupByDb;
use join::JoinDb;
use limit::LimitDb;
use open::OpenDb;
use or::OrDb;
@@ -56,20 +61,21 @@ pub fn add_database_decls(working_set: &mut StateWorkingSet) {
bind_command!(
AliasExpr,
AndDb,
CollectDb,
ColExpr,
CollectDb,
Database,
DescribeDb,
FromDb,
FunctionExpr,
GroupByDb,
QueryDb,
JoinDb,
LimitDb,
ProjectionDb,
OpenDb,
OrderByDb,
OrDb,
OverExpr,
QueryDb,
ProjectionDb,
SchemaDb,
TestingDb,
WhereDb

View File

@@ -68,7 +68,7 @@ impl Command for SchemaDb {
cols.push("db_filename".into());
vals.push(Value::String {
val: sqlite_db.path.to_string_lossy().to_string(),
val: sqlite_db.connection.to_string(),
span,
});