diff --git a/crates/nu-command/src/database/commands/info.rs b/crates/nu-command/src/database/commands/info.rs new file mode 100644 index 000000000..c98f9fa32 --- /dev/null +++ b/crates/nu-command/src/database/commands/info.rs @@ -0,0 +1,245 @@ +use super::super::SQLiteDatabase; +use crate::database::values::db_row::DbRow; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value, +}; +use std::path::PathBuf; + +#[derive(Clone)] +pub struct InfoDb; + +impl Command for InfoDb { + fn name(&self) -> &str { + "db info" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required("db", SyntaxShape::Filepath, "sqlite database file name") + .category(Category::Custom("database".into())) + } + + fn usage(&self) -> &str { + "Show database information." + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Show information of a SQLite database", + example: r#"db info foo.db"#, + result: None, + }] + } + + fn search_terms(&self) -> Vec<&str> { + vec!["database", "info", "SQLite"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let db_file: Spanned = call.req(engine_state, stack, 0)?; + let span = db_file.span; + let mut cols = vec![]; + let mut vals = vec![]; + + let sqlite_db = SQLiteDatabase::try_from_path(db_file.item.as_path(), db_file.span)?; + let conn = sqlite_db.open_connection().map_err(|e| { + ShellError::GenericError( + "Error opening file".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + + let dbs = sqlite_db.get_databases_and_tables(&conn).map_err(|e| { + ShellError::GenericError( + "Error getting databases and tables".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + + cols.push("db_filename".into()); + vals.push(Value::String { + val: db_file.item.to_string_lossy().to_string(), + span, + }); + + for db in dbs { + let tables = db.tables(); + let mut table_list: Vec = vec![]; + let mut table_names = vec![]; + let mut table_values = vec![]; + for table in tables { + let columns = sqlite_db.get_columns(&conn, &table).map_err(|e| { + ShellError::GenericError( + "Error getting database columns".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + // a record of column name = column value + let mut column_info = vec![]; + for t in columns { + let mut col_names = vec![]; + let mut col_values = vec![]; + let fields = t.fields(); + let columns = t.columns(); + for (k, v) in fields.iter().zip(columns.iter()) { + col_names.push(k.clone()); + col_values.push(Value::string(v.clone(), span)); + } + column_info.push(Value::Record { + cols: col_names.clone(), + vals: col_values.clone(), + span, + }); + } + + let constraints = sqlite_db.get_constraints(&conn, &table).map_err(|e| { + ShellError::GenericError( + "Error getting DB constraints".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + let mut constraint_info = vec![]; + for constraint in constraints { + let mut con_cols = vec![]; + let mut con_vals = vec![]; + let fields = constraint.fields(); + let columns = constraint.columns(); + for (k, v) in fields.iter().zip(columns.iter()) { + con_cols.push(k.clone()); + con_vals.push(Value::string(v.clone(), span)); + } + constraint_info.push(Value::Record { + cols: con_cols.clone(), + vals: con_vals.clone(), + span, + }); + } + + let foreign_keys = sqlite_db.get_foreign_keys(&conn, &table).map_err(|e| { + ShellError::GenericError( + "Error getting DB Foreign Keys".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + let mut foreign_key_info = vec![]; + for fk in foreign_keys { + let mut fk_cols = vec![]; + let mut fk_vals = vec![]; + let fields = fk.fields(); + let columns = fk.columns(); + for (k, v) in fields.iter().zip(columns.iter()) { + fk_cols.push(k.clone()); + fk_vals.push(Value::string(v.clone(), span)); + } + foreign_key_info.push(Value::Record { + cols: fk_cols.clone(), + vals: fk_vals.clone(), + span, + }); + } + + let indexes = sqlite_db.get_indexes(&conn, &table).map_err(|e| { + ShellError::GenericError( + "Error getting DB Indexes".into(), + e.to_string(), + Some(span), + None, + Vec::new(), + ) + })?; + let mut index_info = vec![]; + for index in indexes { + let mut idx_cols = vec![]; + let mut idx_vals = vec![]; + let fields = index.fields(); + let columns = index.columns(); + for (k, v) in fields.iter().zip(columns.iter()) { + idx_cols.push(k.clone()); + idx_vals.push(Value::string(v.clone(), span)); + } + index_info.push(Value::Record { + cols: idx_cols.clone(), + vals: idx_vals.clone(), + span, + }); + } + + table_names.push(table.name); + table_values.push(Value::Record { + cols: vec![ + "columns".into(), + "constraints".into(), + "foreign_keys".into(), + "indexes".into(), + ], + vals: vec![ + Value::List { + vals: column_info, + span, + }, + Value::List { + vals: constraint_info, + span, + }, + Value::List { + vals: foreign_key_info, + span, + }, + Value::List { + vals: index_info, + span, + }, + ], + span, + }); + } + table_list.push(Value::Record { + cols: table_names, + vals: table_values, + span, + }); + + cols.push("databases".into()); + let mut rcols = vec![]; + let mut rvals = vec![]; + rcols.push("name".into()); + rvals.push(Value::string(db.name().to_string(), span)); + rcols.push("tables".into()); + rvals.append(&mut table_list); + vals.push(Value::Record { + cols: rcols, + vals: rvals, + span, + }); + } + + Ok(PipelineData::Value( + Value::Record { cols, vals, span }, + None, + )) + } +} diff --git a/crates/nu-command/src/database/commands/mod.rs b/crates/nu-command/src/database/commands/mod.rs index cf14629f1..2042e908e 100644 --- a/crates/nu-command/src/database/commands/mod.rs +++ b/crates/nu-command/src/database/commands/mod.rs @@ -2,6 +2,7 @@ mod collect; mod command; mod describe; mod from; +mod info; mod open; mod query; mod select; @@ -11,6 +12,7 @@ use collect::CollectDb; use command::Database; use describe::DescribeDb; use from::FromDb; +use info::InfoDb; use nu_protocol::engine::StateWorkingSet; use open::OpenDb; use query::QueryDb; @@ -27,5 +29,5 @@ pub fn add_database_decls(working_set: &mut StateWorkingSet) { } // Series commands - bind_command!(CollectDb, Database, DescribeDb, FromDb, QueryDb, SelectDb, OpenDb); + bind_command!(CollectDb, Database, DescribeDb, FromDb, QueryDb, SelectDb, OpenDb, InfoDb); } diff --git a/crates/nu-command/src/database/mod.rs b/crates/nu-command/src/database/mod.rs index 6b7c7cf12..5a11974a8 100644 --- a/crates/nu-command/src/database/mod.rs +++ b/crates/nu-command/src/database/mod.rs @@ -2,4 +2,7 @@ mod commands; mod values; pub use commands::add_database_decls; -pub(crate) use values::SQLiteDatabase; +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, +}; diff --git a/crates/nu-command/src/database/values/db.rs b/crates/nu-command/src/database/values/db.rs new file mode 100644 index 000000000..1b9dd5c46 --- /dev/null +++ b/crates/nu-command/src/database/values/db.rs @@ -0,0 +1,27 @@ +use crate::database::values::db_table::DbTable; + +// Thank you gobang +// https://github.com/TaKO8Ki/gobang/blob/main/database-tree/src/lib.rs + +#[derive(Clone, PartialEq, Debug)] +pub struct Db { + pub name: String, + pub tables: Vec, +} + +impl Db { + pub fn new(database: String, tables: Vec) -> Self { + Self { + name: database, + tables, + } + } + + pub fn name(&self) -> &str { + self.name.as_str() + } + + pub fn tables(&self) -> Vec { + self.tables.clone() + } +} diff --git a/crates/nu-command/src/database/values/db_column.rs b/crates/nu-command/src/database/values/db_column.rs new file mode 100644 index 000000000..03010ad2b --- /dev/null +++ b/crates/nu-command/src/database/values/db_column.rs @@ -0,0 +1,51 @@ +use crate::database::values::db_row::DbRow; + +#[derive(Debug)] +pub struct DbColumn { + /// Column Index + pub cid: Option, + /// Column Name + pub name: Option, + /// Column Type + pub r#type: Option, + /// Column has a NOT NULL constraint + pub notnull: Option, + /// Column DEFAULT Value + pub default: Option, + /// Column is part of the PRIMARY KEY + pub pk: Option, +} + +impl DbRow for DbColumn { + fn fields(&self) -> Vec { + vec![ + "cid".to_string(), + "name".to_string(), + "type".to_string(), + "notnull".to_string(), + "default".to_string(), + "pk".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.cid + .as_ref() + .map_or(String::new(), |cid| cid.to_string()), + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.r#type + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.notnull + .as_ref() + .map_or(String::new(), |notnull| notnull.to_string()), + self.default + .as_ref() + .map_or(String::new(), |default| default.to_string()), + self.pk.as_ref().map_or(String::new(), |pk| pk.to_string()), + ] + } +} diff --git a/crates/nu-command/src/database/values/db_constraint.rs b/crates/nu-command/src/database/values/db_constraint.rs new file mode 100644 index 000000000..e067856fb --- /dev/null +++ b/crates/nu-command/src/database/values/db_constraint.rs @@ -0,0 +1,26 @@ +use crate::database::values::db_row::DbRow; + +#[derive(Debug)] +pub struct DbConstraint { + pub name: String, + pub column_name: String, + pub origin: String, +} + +impl DbRow for DbConstraint { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "origin".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name.to_string(), + self.column_name.to_string(), + self.origin.to_string(), + ] + } +} diff --git a/crates/nu-command/src/database/values/db_foreignkey.rs b/crates/nu-command/src/database/values/db_foreignkey.rs new file mode 100644 index 000000000..97b839b5c --- /dev/null +++ b/crates/nu-command/src/database/values/db_foreignkey.rs @@ -0,0 +1,32 @@ +use crate::database::values::db_row::DbRow; + +#[derive(Debug)] +pub struct DbForeignKey { + pub column_name: Option, + pub ref_table: Option, + pub ref_column: Option, +} + +impl DbRow for DbForeignKey { + fn fields(&self) -> Vec { + vec![ + "column_name".to_string(), + "ref_table".to_string(), + "ref_column".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.column_name + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_table + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + self.ref_column + .as_ref() + .map_or(String::new(), |r#type| r#type.to_string()), + ] + } +} diff --git a/crates/nu-command/src/database/values/db_index.rs b/crates/nu-command/src/database/values/db_index.rs new file mode 100644 index 000000000..98a6ed935 --- /dev/null +++ b/crates/nu-command/src/database/values/db_index.rs @@ -0,0 +1,32 @@ +use crate::database::values::db_row::DbRow; + +#[derive(Debug)] +pub struct DbIndex { + pub name: Option, + pub column_name: Option, + pub seqno: Option, +} + +impl DbRow for DbIndex { + fn fields(&self) -> Vec { + vec![ + "name".to_string(), + "column_name".to_string(), + "seqno".to_string(), + ] + } + + fn columns(&self) -> Vec { + vec![ + self.name + .as_ref() + .map_or(String::new(), |name| name.to_string()), + self.column_name + .as_ref() + .map_or(String::new(), |column_name| column_name.to_string()), + self.seqno + .as_ref() + .map_or(String::new(), |seqno| seqno.to_string()), + ] + } +} diff --git a/crates/nu-command/src/database/values/db_row.rs b/crates/nu-command/src/database/values/db_row.rs new file mode 100644 index 000000000..4a7be754f --- /dev/null +++ b/crates/nu-command/src/database/values/db_row.rs @@ -0,0 +1,4 @@ +pub trait DbRow: std::marker::Send { + fn fields(&self) -> Vec; + fn columns(&self) -> Vec; +} diff --git a/crates/nu-command/src/database/values/db_schema.rs b/crates/nu-command/src/database/values/db_schema.rs new file mode 100644 index 000000000..55fbe7409 --- /dev/null +++ b/crates/nu-command/src/database/values/db_schema.rs @@ -0,0 +1,7 @@ +use crate::database::values::db_table::DbTable; + +#[derive(Clone, PartialEq, Debug)] +pub struct DbSchema { + pub name: String, + pub tables: Vec, +} diff --git a/crates/nu-command/src/database/values/db_table.rs b/crates/nu-command/src/database/values/db_table.rs new file mode 100644 index 000000000..36d9b8302 --- /dev/null +++ b/crates/nu-command/src/database/values/db_table.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, PartialEq)] +pub struct DbTable { + pub name: String, + pub create_time: Option>, + pub update_time: Option>, + pub engine: Option, + pub schema: Option, +} diff --git a/crates/nu-command/src/database/values/mod.rs b/crates/nu-command/src/database/values/mod.rs index b221889db..5a941a750 100644 --- a/crates/nu-command/src/database/values/mod.rs +++ b/crates/nu-command/src/database/values/mod.rs @@ -1,3 +1,14 @@ -mod sqlite; +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 sqlite; -pub(crate) use sqlite::SQLiteDatabase; +pub use sqlite::{ + 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, +}; diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index b4e5aa272..1fbe3e591 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -1,15 +1,17 @@ +use crate::database::values::{ + 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 sqlparser::ast::Query; use std::{ fs::File, io::Read, path::{Path, PathBuf}, }; -use nu_protocol::{CustomValue, PipelineData, ShellError, Span, Spanned, Value}; -use rusqlite::{types::ValueRef, Connection, Row}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use sqlparser::ast::Query; - const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes(); #[derive(Debug)] @@ -184,6 +186,207 @@ impl SQLiteDatabase { span, } } + + pub fn open_connection(&self) -> Result { + let conn = match Connection::open(self.path.to_string_lossy().to_string()) { + Ok(conn) => conn, + Err(err) => return Err(err), + }; + + Ok(conn) + } + + pub fn get_databases_and_tables(&self, conn: &Connection) -> Result, rusqlite::Error> { + // let conn = open_connection(path)?; + let mut db_query = conn.prepare("SELECT name FROM pragma_database_list")?; + + let databases = db_query.query_map([], |row| { + let name: String = row.get(0)?; + Ok(Db::new(name, self.get_tables(conn)?)) + })?; + + let mut db_list = vec![]; + for db in databases { + db_list.push(db?); + } + + Ok(db_list) + } + + pub fn get_databases(&self, conn: &Connection) -> Result, rusqlite::Error> { + let mut db_query = conn.prepare("SELECT name FROM pragma_database_list")?; + + let mut db_list = vec![]; + let _ = db_query.query_map([], |row| { + let name: String = row.get(0)?; + db_list.push(name); + Ok(()) + })?; + + Ok(db_list) + } + + pub fn get_tables(&self, conn: &Connection) -> Result, rusqlite::Error> { + let mut table_names = + conn.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")?; + let rows = table_names.query_map([], |row| row.get(0))?; + let mut tables = Vec::new(); + + for row in rows { + let table_name: String = row?; + tables.push(DbTable { + name: table_name, + create_time: None, + update_time: None, + engine: None, + schema: None, + }) + } + + Ok(tables.into_iter().collect()) + } + + fn get_column_info(&self, row: &Row) -> Result { + let dbc = DbColumn { + cid: row.get("cid")?, + name: row.get("name")?, + r#type: row.get("type")?, + notnull: row.get("notnull")?, + default: row.get("dflt_value")?, + pk: row.get("pk")?, + }; + Ok(dbc) + } + + pub fn get_columns( + &self, + conn: &Connection, + table: &DbTable, + ) -> Result, rusqlite::Error> { + let mut column_names = conn.prepare(&format!( + "SELECT * FROM pragma_table_info('{}');", + table.name + ))?; + + let mut columns: Vec = Vec::new(); + let rows = column_names.query_and_then([], |row| self.get_column_info(row))?; + + for row in rows { + columns.push(row?); + } + + Ok(columns) + } + + fn get_constraint_info(&self, row: &Row) -> Result { + let dbc = DbConstraint { + name: row.get("index_name")?, + column_name: row.get("column_name")?, + origin: row.get("origin")?, + }; + Ok(dbc) + } + + pub fn get_constraints( + &self, + conn: &Connection, + table: &DbTable, + ) -> Result, rusqlite::Error> { + let mut column_names = conn.prepare(&format!( + " + SELECT + p.origin, + s.name AS index_name, + i.name AS column_name + FROM + sqlite_master s + JOIN pragma_index_list(s.tbl_name) p ON s.name = p.name, + pragma_index_info(s.name) i + WHERE + s.type = 'index' + AND tbl_name = '{}' + AND NOT p.origin = 'c' + ", + &table.name + ))?; + + let mut constraints: Vec = Vec::new(); + let rows = column_names.query_and_then([], |row| self.get_constraint_info(row))?; + + for row in rows { + constraints.push(row?); + } + + Ok(constraints) + } + + fn get_foreign_keys_info(&self, row: &Row) -> Result { + let dbc = DbForeignKey { + column_name: row.get("from")?, + ref_table: row.get("table")?, + ref_column: row.get("to")?, + }; + Ok(dbc) + } + + pub fn get_foreign_keys( + &self, + conn: &Connection, + table: &DbTable, + ) -> Result, rusqlite::Error> { + let mut column_names = conn.prepare(&format!( + "SELECT p.`from`, p.`to`, p.`table` FROM pragma_foreign_key_list('{}') p", + &table.name + ))?; + + let mut foreign_keys: Vec = Vec::new(); + let rows = column_names.query_and_then([], |row| self.get_foreign_keys_info(row))?; + + for row in rows { + foreign_keys.push(row?); + } + + Ok(foreign_keys) + } + + fn get_index_info(&self, row: &Row) -> Result { + let dbc = DbIndex { + name: row.get("index_name")?, + column_name: row.get("name")?, + seqno: row.get("seqno")?, + }; + Ok(dbc) + } + + pub fn get_indexes( + &self, + conn: &Connection, + table: &DbTable, + ) -> Result, rusqlite::Error> { + let mut column_names = conn.prepare(&format!( + " + SELECT + m.name AS index_name, + p.* + FROM + sqlite_master m, + pragma_index_info(m.name) p + WHERE + m.type = 'index' + AND m.tbl_name = '{}' + ", + &table.name, + ))?; + + let mut indexes: Vec = Vec::new(); + let rows = column_names.query_and_then([], |row| self.get_index_info(row))?; + + for row in rows { + indexes.push(row?); + } + + Ok(indexes) + } } impl CustomValue for SQLiteDatabase { @@ -329,7 +532,7 @@ fn read_entire_sqlite_db(conn: Connection, call_span: Span) -> Result Value { +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(); let colnames = colnamestr.iter().map(|s| s.to_string()).collect(); @@ -347,7 +550,7 @@ fn convert_sqlite_row_to_nu_value(row: &Row, span: Span) -> Value { } } -fn convert_sqlite_value_to_nu_value(value: ValueRef, span: Span) -> Value { +pub fn convert_sqlite_value_to_nu_value(value: ValueRef, span: Span) -> Value { match value { ValueRef::Null => Value::Nothing { span }, ValueRef::Integer(i) => Value::Int { val: i, span }, @@ -469,3 +672,82 @@ mod test { assert_eq!(converted_db, expected); } } + +//---------------------------------------------------- +pub fn open_connection_in_memory() -> Result { + let db = match Connection::open_in_memory() { + Ok(conn) => conn, + Err(err) => { + return Err(ShellError::GenericError( + "Failed to open SQLite connection in memory".into(), + err.to_string(), + Some(Span::test_data()), + None, + Vec::new(), + )) + } + }; + + Ok(db) +} + +pub fn open_and_read_sqlite_db( + path: &Path, + call_span: Span, +) -> Result { + let path = path.to_string_lossy().to_string(); + + match Connection::open(path) { + Ok(conn) => match read_sqlite_db(conn, call_span) { + Ok(data) => Ok(data), + Err(err) => Err(ShellError::GenericError( + "Failed to read from SQLite database".into(), + err.to_string(), + Some(call_span), + None, + Vec::new(), + )), + }, + Err(err) => Err(ShellError::GenericError( + "Failed to open SQLite database".into(), + err.to_string(), + Some(call_span), + None, + Vec::new(), + )), + } +} + +pub fn read_sqlite_db(conn: Connection, call_span: Span) -> Result { + let mut table_names: Vec = Vec::new(); + let mut tables: Vec = Vec::new(); + + let mut get_table_names = + conn.prepare("SELECT name from sqlite_master where type = 'table'")?; + let rows = get_table_names.query_map([], |row| row.get(0))?; + + for row in rows { + let table_name: String = row?; + table_names.push(table_name.clone()); + + let mut rows = Vec::new(); + let mut table_stmt = conn.prepare(&format!("select * from [{}]", table_name))?; + let mut table_rows = table_stmt.query([])?; + while let Some(table_row) = table_rows.next()? { + rows.push(convert_sqlite_row_to_nu_value(table_row, call_span)) + } + + let table_record = Value::List { + vals: rows, + span: call_span, + }; + + tables.push(table_record); + } + + Ok(Value::Record { + cols: table_names, + vals: tables, + span: call_span, + }) +}