db info command (#5335)

* db info WIP

* working now

* clippy
This commit is contained in:
Darren Schroeder 2022-04-26 14:20:59 -05:00 committed by GitHub
parent be3f0edc97
commit 5319544481
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 742 additions and 12 deletions

View file

@ -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<Example> {
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<PipelineData, ShellError> {
let db_file: Spanned<PathBuf> = 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<Value> = 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,
))
}
}

View file

@ -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);
}

View file

@ -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,
};

View file

@ -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<DbTable>,
}
impl Db {
pub fn new(database: String, tables: Vec<DbTable>) -> Self {
Self {
name: database,
tables,
}
}
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn tables(&self) -> Vec<DbTable> {
self.tables.clone()
}
}

View file

@ -0,0 +1,51 @@
use crate::database::values::db_row::DbRow;
#[derive(Debug)]
pub struct DbColumn {
/// Column Index
pub cid: Option<i32>,
/// Column Name
pub name: Option<String>,
/// Column Type
pub r#type: Option<String>,
/// Column has a NOT NULL constraint
pub notnull: Option<i16>,
/// Column DEFAULT Value
pub default: Option<String>,
/// Column is part of the PRIMARY KEY
pub pk: Option<i16>,
}
impl DbRow for DbColumn {
fn fields(&self) -> Vec<String> {
vec![
"cid".to_string(),
"name".to_string(),
"type".to_string(),
"notnull".to_string(),
"default".to_string(),
"pk".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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()),
]
}
}

View file

@ -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<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"origin".to_string(),
]
}
fn columns(&self) -> Vec<String> {
vec![
self.name.to_string(),
self.column_name.to_string(),
self.origin.to_string(),
]
}
}

View file

@ -0,0 +1,32 @@
use crate::database::values::db_row::DbRow;
#[derive(Debug)]
pub struct DbForeignKey {
pub column_name: Option<String>,
pub ref_table: Option<String>,
pub ref_column: Option<String>,
}
impl DbRow for DbForeignKey {
fn fields(&self) -> Vec<String> {
vec![
"column_name".to_string(),
"ref_table".to_string(),
"ref_column".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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()),
]
}
}

View file

@ -0,0 +1,32 @@
use crate::database::values::db_row::DbRow;
#[derive(Debug)]
pub struct DbIndex {
pub name: Option<String>,
pub column_name: Option<String>,
pub seqno: Option<i16>,
}
impl DbRow for DbIndex {
fn fields(&self) -> Vec<String> {
vec![
"name".to_string(),
"column_name".to_string(),
"seqno".to_string(),
]
}
fn columns(&self) -> Vec<String> {
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()),
]
}
}

View file

@ -0,0 +1,4 @@
pub trait DbRow: std::marker::Send {
fn fields(&self) -> Vec<String>;
fn columns(&self) -> Vec<String>;
}

View file

@ -0,0 +1,7 @@
use crate::database::values::db_table::DbTable;
#[derive(Clone, PartialEq, Debug)]
pub struct DbSchema {
pub name: String,
pub tables: Vec<DbTable>,
}

View file

@ -0,0 +1,8 @@
#[derive(Debug, Clone, PartialEq)]
pub struct DbTable {
pub name: String,
pub create_time: Option<chrono::DateTime<chrono::Utc>>,
pub update_time: Option<chrono::DateTime<chrono::Utc>>,
pub engine: Option<String>,
pub schema: Option<String>,
}

View file

@ -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,
};

View file

@ -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<Connection, rusqlite::Error> {
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<Vec<Db>, 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<Vec<String>, 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<Vec<DbTable>, 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<DbColumn, rusqlite::Error> {
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<Vec<DbColumn>, rusqlite::Error> {
let mut column_names = conn.prepare(&format!(
"SELECT * FROM pragma_table_info('{}');",
table.name
))?;
let mut columns: Vec<DbColumn> = 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<DbConstraint, rusqlite::Error> {
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<Vec<DbConstraint>, 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<DbConstraint> = 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<DbForeignKey, rusqlite::Error> {
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<Vec<DbForeignKey>, 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<DbForeignKey> = 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<DbIndex, rusqlite::Error> {
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<Vec<DbIndex>, 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<DbIndex> = 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, rus
span: call_span,
})
}
fn convert_sqlite_row_to_nu_value(row: &Row, span: Span) -> 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<Connection, ShellError> {
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<Value, nu_protocol::ShellError> {
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<Value, rusqlite::Error> {
let mut table_names: Vec<String> = Vec::new();
let mut tables: Vec<Value> = 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,
})
}