Database commands (#5343)

* dabase access commands

* select expression

* select using expressions

* cargo fmt
This commit is contained in:
Fernando Herrera 2022-04-27 11:52:31 +01:00 committed by GitHub
parent cd5199de31
commit 5c9fe85ec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 745 additions and 154 deletions

1
Cargo.lock generated
View File

@ -4102,6 +4102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9a527b68048eb95495a1508f6c8395c8defcff5ecdbe8ad4106d08a2ef2a3c"
dependencies = [
"log",
"serde",
]
[[package]]

View File

@ -81,7 +81,7 @@ which = { version = "4.2.2", optional = true }
reedline = { git = "https://github.com/nushell/reedline", branch = "main", features = ["bashisms"]}
wax = { version = "0.4.0", features = ["diagnostics"] }
rusqlite = { version = "0.27.0", features = ["bundled"], optional = true }
sqlparser = { version = "0.16.0", optional = true }
sqlparser = { version = "0.16.0", features = ["serde"], optional = true }
[target.'cfg(unix)'.dependencies]
umask = "1.0.0"

View File

@ -6,19 +6,23 @@ mod open;
mod query;
mod schema;
mod select;
mod utils;
// Temporal module to create Query objects
mod testing;
use testing::TestingDb;
use nu_protocol::engine::StateWorkingSet;
use collect::CollectDb;
use command::Database;
use describe::DescribeDb;
use from::FromDb;
use nu_protocol::engine::StateWorkingSet;
use open::OpenDb;
use query::QueryDb;
use schema::SchemaDb;
use select::SelectDb;
use select::ProjectionDb;
pub fn add_database_decls(working_set: &mut StateWorkingSet) {
pub fn add_commands_decls(working_set: &mut StateWorkingSet) {
macro_rules! bind_command {
( $command:expr ) => {
working_set.add_decl(Box::new($command));
@ -29,5 +33,15 @@ pub fn add_database_decls(working_set: &mut StateWorkingSet) {
}
// Series commands
bind_command!(CollectDb, Database, DescribeDb, FromDb, QueryDb, SelectDb, OpenDb, SchemaDb);
bind_command!(
CollectDb,
Database,
DescribeDb,
FromDb,
QueryDb,
ProjectionDb,
OpenDb,
SchemaDb,
TestingDb
);
}

View File

@ -1,5 +1,5 @@
use super::super::SQLiteDatabase;
use crate::database::values::db_row::DbRow;
use crate::database::values::definitions::db_row::DbRow;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},

View File

@ -1,16 +1,16 @@
use super::{super::SQLiteDatabase, utils::extract_strings};
use super::{super::values::dsl::SelectDb, super::SQLiteDatabase};
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
};
use sqlparser::ast::{Expr, Ident, Query, Select, SelectItem, SetExpr};
use sqlparser::ast::{Query, Select, SelectItem, SetExpr};
#[derive(Clone)]
pub struct SelectDb;
pub struct ProjectionDb;
impl Command for SelectDb {
impl Command for ProjectionDb {
fn name(&self) -> &str {
"db select"
}
@ -21,7 +21,7 @@ impl Command for SelectDb {
fn signature(&self) -> Signature {
Signature::build(self.name())
.required(
.rest(
"select",
SyntaxShape::Any,
"Select expression(s) on the table",
@ -42,7 +42,7 @@ impl Command for SelectDb {
},
Example {
description: "selects columns from a database",
example: "db open db.mysql | db select [a, b, c]",
example: "db open db.mysql | db select a b c",
result: None,
},
]
@ -55,20 +55,24 @@ impl Command for SelectDb {
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let value: Value = call.req(engine_state, stack, 0)?;
let expressions = extract_strings(value)?;
let vals: Vec<Value> = call.rest(engine_state, stack, 0)?;
let value = Value::List {
vals,
span: call.head,
};
let projection = SelectDb::extract_selects(value)?;
let mut db = SQLiteDatabase::try_from_pipeline(input, call.head)?;
db.query = match db.query {
None => Some(create_query(expressions)),
Some(query) => Some(modify_query(query, expressions)),
None => Some(create_query(projection)),
Some(query) => Some(modify_query(query, projection)),
};
Ok(db.into_value(call.head).into_pipeline_data())
}
}
fn create_query(expressions: Vec<String>) -> Query {
fn create_query(expressions: Vec<SelectItem>) -> Query {
Query {
with: None,
body: SetExpr::Select(Box::new(create_select(expressions))),
@ -80,7 +84,7 @@ fn create_query(expressions: Vec<String>) -> Query {
}
}
fn modify_query(mut query: Query, expressions: Vec<String>) -> Query {
fn modify_query(mut query: Query, expressions: Vec<SelectItem>) -> Query {
query.body = match query.body {
SetExpr::Select(select) => SetExpr::Select(Box::new(modify_select(select, expressions))),
_ => SetExpr::Select(Box::new(create_select(expressions))),
@ -89,18 +93,18 @@ fn modify_query(mut query: Query, expressions: Vec<String>) -> Query {
query
}
fn modify_select(select: Box<Select>, expressions: Vec<String>) -> Select {
fn modify_select(select: Box<Select>, projection: Vec<SelectItem>) -> Select {
Select {
projection: create_projection(expressions),
projection,
..select.as_ref().clone()
}
}
fn create_select(expressions: Vec<String>) -> Select {
fn create_select(projection: Vec<SelectItem>) -> Select {
Select {
distinct: false,
top: None,
projection: create_projection(expressions),
projection,
into: None,
from: Vec::new(),
lateral_views: Vec::new(),
@ -112,20 +116,3 @@ fn create_select(expressions: Vec<String>) -> Select {
having: None,
}
}
// This function needs more work
// It needs to define alias and functions in the columns
// I assume we will need to define expressions for the columns instead of strings
fn create_projection(expressions: Vec<String>) -> Vec<SelectItem> {
expressions
.into_iter()
.map(|expression| {
let expr = Expr::Identifier(Ident {
value: expression,
quote_style: None,
});
SelectItem::UnnamedExpr(expr)
})
.collect()
}

View File

@ -0,0 +1,76 @@
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned, SyntaxShape,
Value,
};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;
#[derive(Clone)]
pub struct TestingDb;
impl Command for TestingDb {
fn name(&self) -> &str {
"db testing"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required(
"query",
SyntaxShape::String,
"SQL to execute to create the query object",
)
.category(Category::Custom("database".into()))
}
fn usage(&self) -> &str {
"Create query object"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "",
example: "",
result: None,
}]
}
fn search_terms(&self) -> Vec<&str> {
vec!["database", "SQLite"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let sql: Spanned<String> = call.req(engine_state, stack, 0)?;
let dialect = GenericDialect {}; // or AnsiDialect, or your own dialect ...
let ast = Parser::parse_sql(&dialect, sql.item.as_str()).map_err(|e| {
ShellError::GenericError(
"Error creating AST".into(),
e.to_string(),
Some(sql.span),
None,
Vec::new(),
)
})?;
let value = match ast.get(0) {
None => Value::nothing(call.head),
Some(statement) => Value::String {
val: format!("{:#?}", statement),
span: call.head,
},
};
Ok(value.into_pipeline_data())
}
}

View File

@ -1,15 +0,0 @@
use nu_protocol::{FromValue, ShellError, Value};
pub fn extract_strings(value: Value) -> Result<Vec<String>, ShellError> {
match (
<String as FromValue>::from_value(&value),
<Vec<String> as FromValue>::from_value(&value),
) {
(Ok(col), Err(_)) => Ok(vec![col]),
(Err(_), Ok(cols)) => Ok(cols),
_ => Err(ShellError::IncompatibleParametersSingle(
"Expected a string or list of strings".into(),
value.span()?,
)),
}
}

View File

@ -0,0 +1,71 @@
use crate::database::values::dsl::SelectDb;
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape,
};
use sqlparser::ast::{Ident, SelectItem};
#[derive(Clone)]
pub struct AliasExpr;
impl Command for AliasExpr {
fn name(&self) -> &str {
"db as"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("alias", SyntaxShape::String, "alias name")
.category(Category::Custom("database".into()))
}
fn usage(&self) -> &str {
"Creates an alias for a column selection"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Creates an alias for a a column selection",
example: "db col name_a | db as new_a",
result: None,
}]
}
fn search_terms(&self) -> Vec<&str> {
vec!["database", "column", "expression"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let alias: String = call.req(engine_state, stack, 0)?;
let select = SelectDb::try_from_pipeline(input, call.head)?;
let select = match select.into_native() {
SelectItem::UnnamedExpr(expr) => SelectItem::ExprWithAlias {
expr,
alias: Ident {
value: alias,
quote_style: None,
},
},
SelectItem::ExprWithAlias { expr, .. } => SelectItem::ExprWithAlias {
expr,
alias: Ident {
value: alias,
quote_style: None,
},
},
select => select,
};
let select: SelectDb = select.into();
Ok(select.into_value(call.head).into_pipeline_data())
}
}

View File

@ -0,0 +1,71 @@
use crate::database::values::dsl::{ExprDb, SelectDb};
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
};
use sqlparser::ast::{Ident, ObjectName, SelectItem};
#[derive(Clone)]
pub struct ColExpr;
impl Command for ColExpr {
fn name(&self) -> &str {
"db col"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("name", SyntaxShape::String, "column name")
.category(Category::Custom("database".into()))
}
fn usage(&self) -> &str {
"Creates column expression for database"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Creates a named column expression",
example: "col name_1",
result: None,
}]
}
fn search_terms(&self) -> Vec<&str> {
vec!["database", "column", "expression"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let name: Value = call.req(engine_state, stack, 0)?;
let select = match name {
Value::String { val, .. } if val == "*" => SelectItem::Wildcard,
Value::String { val, .. } if val.contains('.') => {
let values = val
.split('.')
.map(|part| Ident {
value: part.to_string(),
quote_style: None,
})
.collect::<Vec<Ident>>();
SelectItem::QualifiedWildcard(ObjectName(values))
}
_ => {
let expr = ExprDb::try_from_value(name)?;
SelectItem::UnnamedExpr(expr.into_native())
}
};
let selection: SelectDb = select.into();
Ok(selection.into_value(call.head).into_pipeline_data())
}
}

View File

@ -0,0 +1,21 @@
mod alias;
mod col;
use nu_protocol::engine::StateWorkingSet;
use alias::AliasExpr;
use col::ColExpr;
pub fn add_expression_decls(working_set: &mut StateWorkingSet) {
macro_rules! bind_command {
( $command:expr ) => {
working_set.add_decl(Box::new($command));
};
( $( $command:expr ),* ) => {
$( working_set.add_decl(Box::new($command)); )*
};
}
// Series commands
bind_command!(AliasExpr, ColExpr);
}

View File

@ -1,8 +1,16 @@
mod commands;
mod values;
pub use commands::add_database_decls;
mod expressions;
pub use commands::add_commands_decls;
pub use expressions::add_expression_decls;
use nu_protocol::engine::StateWorkingSet;
pub use values::{
convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_and_read_sqlite_db,
open_connection_in_memory, read_sqlite_db, SQLiteDatabase,
};
pub fn add_database_decls(working_set: &mut StateWorkingSet) {
add_commands_decls(working_set);
add_expression_decls(working_set);
}

View File

@ -1,4 +1,4 @@
use crate::database::values::db_table::DbTable;
use super::db_table::DbTable;
// Thank you gobang
// https://github.com/TaKO8Ki/gobang/blob/main/database-tree/src/lib.rs

View File

@ -1,4 +1,4 @@
use crate::database::values::db_row::DbRow;
use crate::database::values::definitions::db_row::DbRow;
#[derive(Debug)]
pub struct DbColumn {

View File

@ -1,4 +1,4 @@
use crate::database::values::db_row::DbRow;
use super::db_row::DbRow;
#[derive(Debug)]
pub struct DbConstraint {

View File

@ -1,4 +1,4 @@
use crate::database::values::db_row::DbRow;
use super::db_row::DbRow;
#[derive(Debug)]
pub struct DbForeignKey {

View File

@ -1,4 +1,4 @@
use crate::database::values::db_row::DbRow;
use super::db_row::DbRow;
#[derive(Debug)]
pub struct DbIndex {

View File

@ -1,4 +1,4 @@
use crate::database::values::db_table::DbTable;
use super::db_table::DbTable;
#[derive(Clone, PartialEq, Debug)]
pub struct DbSchema {

View File

@ -0,0 +1,8 @@
pub mod db;
pub mod db_column;
pub mod db_constraint;
pub mod db_foreignkey;
pub mod db_index;
pub mod db_row;
pub mod db_schema;
pub mod db_table;

View File

@ -0,0 +1,160 @@
use nu_protocol::{CustomValue, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use sqlparser::ast::{Expr, Ident};
#[derive(Debug, Serialize, Deserialize)]
pub struct ExprDb(Expr);
// Referenced access to the native expression
impl AsRef<Expr> for ExprDb {
fn as_ref(&self) -> &Expr {
&self.0
}
}
impl AsMut<Expr> for ExprDb {
fn as_mut(&mut self) -> &mut Expr {
&mut self.0
}
}
impl From<Expr> for ExprDb {
fn from(expr: Expr) -> Self {
Self(expr)
}
}
impl CustomValue for ExprDb {
fn clone_value(&self, span: Span) -> Value {
let cloned = Self(self.0.clone());
Value::CustomValue {
val: Box::new(cloned),
span,
}
}
fn value_string(&self) -> String {
self.typetag_name().to_string()
}
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
Ok(self.to_value(span))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn typetag_name(&self) -> &'static str {
"DB expresssion"
}
fn typetag_deserialize(&self) {
unimplemented!("typetag_deserialize")
}
}
impl ExprDb {
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
match value {
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
Some(expr) => Ok(Self(expr.0.clone())),
None => Err(ShellError::CantConvert(
"db expression".into(),
"non-expression".into(),
span,
None,
)),
},
Value::String { val, .. } => Ok(Expr::Identifier(Ident {
value: val,
quote_style: None,
})
.into()),
x => Err(ShellError::CantConvert(
"database".into(),
x.get_type().to_string(),
x.span()?,
None,
)),
}
}
// pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
// let value = input.into_value(span);
// Self::try_from_value(value)
// }
// pub fn into_value(self, span: Span) -> Value {
// Value::CustomValue {
// val: Box::new(self),
// span,
// }
// }
pub fn into_native(self) -> Expr {
self.0
}
pub fn to_value(&self, span: Span) -> Value {
ExprDb::expr_to_value(self.as_ref(), span)
}
}
impl ExprDb {
pub fn expr_to_value(expr: &Expr, span: Span) -> Value {
match expr {
Expr::Identifier(ident) => {
let cols = vec!["value".into(), "quoted_style".into()];
let val = Value::String {
val: ident.value.to_string(),
span,
};
let style = Value::String {
val: format!("{:?}", ident.quote_style),
span,
};
Value::Record {
cols,
vals: vec![val, style],
span,
}
}
Expr::CompoundIdentifier(_) => todo!(),
Expr::IsNull(_) => todo!(),
Expr::IsNotNull(_) => todo!(),
Expr::IsDistinctFrom(_, _) => todo!(),
Expr::IsNotDistinctFrom(_, _) => todo!(),
Expr::InList { .. } => todo!(),
Expr::InSubquery { .. } => todo!(),
Expr::InUnnest { .. } => todo!(),
Expr::Between { .. } => todo!(),
Expr::BinaryOp { .. } => todo!(),
Expr::UnaryOp { .. } => todo!(),
Expr::Cast { .. } => todo!(),
Expr::TryCast { .. } => todo!(),
Expr::Extract { .. } => todo!(),
Expr::Substring { .. } => todo!(),
Expr::Trim { .. } => todo!(),
Expr::Collate { .. } => todo!(),
Expr::Nested(_) => todo!(),
Expr::Value(_) => todo!(),
Expr::TypedString { .. } => todo!(),
Expr::MapAccess { .. } => todo!(),
Expr::Function(_) => todo!(),
Expr::Case { .. } => todo!(),
Expr::Exists(_) => todo!(),
Expr::Subquery(_) => todo!(),
Expr::ListAgg(_) => todo!(),
Expr::GroupingSets(_) => todo!(),
Expr::Cube(_) => todo!(),
Expr::Rollup(_) => todo!(),
Expr::Tuple(_) => todo!(),
Expr::ArrayIndex { .. } => todo!(),
Expr::Array(_) => todo!(),
}
}
}

View File

@ -0,0 +1,5 @@
mod expression;
mod select_item;
pub(crate) use expression::ExprDb;
pub(crate) use select_item::SelectDb;

View File

@ -0,0 +1,222 @@
use super::ExprDb;
use nu_protocol::{ast::PathMember, CustomValue, PipelineData, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use sqlparser::ast::{Expr, Ident, SelectItem};
#[derive(Debug, Serialize, Deserialize)]
pub struct SelectDb(SelectItem);
// Referenced access to the native expression
impl AsRef<SelectItem> for SelectDb {
fn as_ref(&self) -> &SelectItem {
&self.0
}
}
impl AsMut<SelectItem> for SelectDb {
fn as_mut(&mut self) -> &mut SelectItem {
&mut self.0
}
}
impl From<SelectItem> for SelectDb {
fn from(expr: SelectItem) -> Self {
Self(expr)
}
}
impl CustomValue for SelectDb {
fn clone_value(&self, span: Span) -> Value {
let cloned = Self(self.0.clone());
Value::CustomValue {
val: Box::new(cloned),
span,
}
}
fn value_string(&self) -> String {
self.typetag_name().to_string()
}
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
Ok(self.to_value(span))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn follow_path_int(&self, count: usize, span: Span) -> Result<Value, ShellError> {
let path = PathMember::Int { val: count, span };
SelectDb::select_to_value(self.as_ref(), span).follow_cell_path(&[path])
}
fn follow_path_string(&self, column_name: String, span: Span) -> Result<Value, ShellError> {
let path = PathMember::String {
val: column_name,
span,
};
SelectDb::select_to_value(self.as_ref(), span).follow_cell_path(&[path])
}
fn typetag_name(&self) -> &'static str {
"DB selection"
}
fn typetag_deserialize(&self) {
unimplemented!("typetag_deserialize")
}
}
impl SelectDb {
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
match value {
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
Some(expr) => Ok(Self(expr.0.clone())),
None => Err(ShellError::CantConvert(
"db expression".into(),
"non-expression".into(),
span,
None,
)),
},
Value::String { val, .. } => {
let expr = Expr::Identifier(Ident {
value: val,
quote_style: None,
});
Ok(SelectItem::UnnamedExpr(expr).into())
}
x => Err(ShellError::CantConvert(
"selection".into(),
x.get_type().to_string(),
x.span()?,
None,
)),
}
}
pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
let value = input.into_value(span);
Self::try_from_value(value)
}
pub fn into_value(self, span: Span) -> Value {
Value::CustomValue {
val: Box::new(self),
span,
}
}
pub fn into_native(self) -> SelectItem {
self.0
}
pub fn to_value(&self, span: Span) -> Value {
SelectDb::select_to_value(self.as_ref(), span)
}
}
impl SelectDb {
fn select_to_value(select: &SelectItem, span: Span) -> Value {
match select {
SelectItem::UnnamedExpr(expr) => ExprDb::expr_to_value(expr, span),
SelectItem::ExprWithAlias { expr, alias } => {
let expr = ExprDb::expr_to_value(expr, span);
let val = Value::String {
val: alias.value.to_string(),
span,
};
let style = Value::String {
val: format!("{:?}", alias.quote_style),
span,
};
let cols = vec!["value".into(), "quoted_style".into()];
let alias = Value::Record {
cols,
vals: vec![val, style],
span,
};
let cols = vec!["expression".into(), "alias".into()];
Value::Record {
cols,
vals: vec![expr, alias],
span,
}
}
SelectItem::QualifiedWildcard(object) => {
let vals: Vec<Value> = object
.0
.iter()
.map(|ident| Value::String {
val: ident.value.clone(),
span,
})
.collect();
Value::List { vals, span }
}
SelectItem::Wildcard => Value::String {
val: "*".into(),
span,
},
}
}
// Convenient function to extrac multiple SelectItem that could be inside a
// nushell Value
pub fn extract_selects(value: Value) -> Result<Vec<SelectItem>, ShellError> {
ExtractedSelect::extract_selects(value).map(ExtractedSelect::into_selects)
}
}
// Enum to represent the parsing of the selects from Value
enum ExtractedSelect {
Single(SelectItem),
List(Vec<ExtractedSelect>),
}
impl ExtractedSelect {
fn into_selects(self) -> Vec<SelectItem> {
match self {
Self::Single(select) => vec![select],
Self::List(selects) => selects
.into_iter()
.flat_map(ExtractedSelect::into_selects)
.collect(),
}
}
fn extract_selects(value: Value) -> Result<ExtractedSelect, ShellError> {
match value {
Value::String { val, .. } => {
let expr = Expr::Identifier(Ident {
value: val,
quote_style: None,
});
Ok(ExtractedSelect::Single(SelectItem::UnnamedExpr(expr)))
}
Value::CustomValue { .. } => SelectDb::try_from_value(value)
.map(SelectDb::into_native)
.map(ExtractedSelect::Single),
Value::List { vals, .. } => vals
.into_iter()
.map(Self::extract_selects)
.collect::<Result<Vec<ExtractedSelect>, ShellError>>()
.map(ExtractedSelect::List),
x => Err(ShellError::CantConvert(
"expression".into(),
x.get_type().to_string(),
x.span()?,
None,
)),
}
}
}

View File

@ -1,11 +1,5 @@
pub mod db;
pub mod db_column;
pub mod db_constraint;
pub mod db_foreignkey;
pub mod db_index;
pub mod db_row;
pub mod db_schema;
pub mod db_table;
pub mod definitions;
pub mod dsl;
pub mod sqlite;
pub use sqlite::{

View File

@ -1,10 +1,10 @@
use crate::database::values::{
use crate::database::values::definitions::{
db::Db, db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey,
db_index::DbIndex, db_table::DbTable,
};
use nu_protocol::{CustomValue, PipelineData, ShellError, Span, Spanned, Value};
use rusqlite::{types::ValueRef, Connection, Row};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Serialize};
use sqlparser::ast::Query;
use std::{
fs::File,
@ -14,7 +14,7 @@ use std::{
const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes();
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SQLiteDatabase {
// I considered storing a SQLite connection here, but decided against it because
// 1) YAGNI, 2) it's not obvious how cloning a connection could work, 3) state
@ -23,27 +23,6 @@ pub struct SQLiteDatabase {
pub query: Option<Query>,
}
// Mocked serialization of the object
impl Serialize for SQLiteDatabase {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_none()
}
}
// Mocked deserialization of the object
impl<'de> Deserialize<'de> for SQLiteDatabase {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let path = std::path::Path::new("");
Ok(SQLiteDatabase::new(path))
}
}
impl SQLiteDatabase {
pub fn new(path: &Path) -> Self {
Self {
@ -53,42 +32,56 @@ impl SQLiteDatabase {
}
pub fn try_from_path(path: &Path, span: Span) -> Result<Self, ShellError> {
let mut file = File::open(path).map_err(|e| {
ShellError::GenericError(
"Error opening file".into(),
e.to_string(),
Some(span),
None,
Vec::new(),
)
})?;
let mut file =
File::open(path).map_err(|e| ShellError::ReadingFile(e.to_string(), span))?;
let mut buf: [u8; 16] = [0; 16];
file.read_exact(&mut buf)
.map_err(|e| {
ShellError::GenericError(
"Error reading file header".into(),
e.to_string(),
Some(span),
None,
Vec::new(),
)
})
.map_err(|e| ShellError::ReadingFile(e.to_string(), span))
.and_then(|_| {
if buf == SQLITE_MAGIC_BYTES {
Ok(SQLiteDatabase::new(path))
} else {
Err(ShellError::GenericError(
"Error reading file".into(),
"Not a SQLite file".into(),
Some(span),
None,
Vec::new(),
))
Err(ShellError::ReadingFile("Not a SQLite file".into(), span))
}
})
}
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
match value {
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
Some(db) => Ok(Self {
path: db.path.clone(),
query: db.query.clone(),
}),
None => Err(ShellError::CantConvert(
"database".into(),
"non-database".into(),
span,
None,
)),
},
x => Err(ShellError::CantConvert(
"database".into(),
x.get_type().to_string(),
x.span()?,
None,
)),
}
}
pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
let value = input.into_value(span);
Self::try_from_value(value)
}
pub fn into_value(self, span: Span) -> Value {
Value::CustomValue {
val: Box::new(self),
span,
}
}
pub fn query(&self, sql: &Spanned<String>, call_span: Span) -> Result<Value, ShellError> {
let db = open_sqlite_db(&self.path, call_span)?;
run_sql_query(db, sql).map_err(|e| {
@ -131,41 +124,6 @@ impl SQLiteDatabase {
})
}
pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
match value {
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
Some(db) => Ok(Self {
path: db.path.clone(),
query: db.query.clone(),
}),
None => Err(ShellError::CantConvert(
"database".into(),
"non-database".into(),
span,
None,
)),
},
x => Err(ShellError::CantConvert(
"database".into(),
x.get_type().to_string(),
x.span()?,
None,
)),
}
}
pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
let value = input.into_value(span);
Self::try_from_value(value)
}
pub fn into_value(self, span: Span) -> Value {
Value::CustomValue {
val: Box::new(self),
span,
}
}
pub fn describe(&self, span: Span) -> Value {
let cols = vec!["connection".to_string(), "query".to_string()];
let connection = Value::String {
@ -532,6 +490,7 @@ fn read_entire_sqlite_db(conn: Connection, call_span: Span) -> Result<Value, rus
span: call_span,
})
}
pub fn convert_sqlite_row_to_nu_value(row: &Row, span: Span) -> Value {
let mut vals = Vec::new();
let colnamestr = row.as_ref().column_names().to_vec();

View File

@ -545,6 +545,15 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
#[error("No file to be copied")]
NoFileToBeCopied(),
/// Error while trying to read a file
///
/// ## Resolution
///
/// The error will show the result from a file operation
#[error("Error trying to read file")]
#[diagnostic(code(nu::shell::error_reading_file), url(docsrs))]
ReadingFile(String, #[label("{0}")] Span),
/// A name was not found. Did you mean a different name?
///
/// ## Resolution