use anyhow::Result;
use super::config::Engine;
use super::connection::{Connection, DatabaseObject};
use super::query::{Cell, QueryResult};
use super::{mysql, postgres, sqlite};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnDef {
pub name: String,
pub type_name: String,
pub nullable: bool,
pub default: Option<String>,
pub is_primary_key: bool,
pub mysql_extra: MySqlColumnExtra,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MySqlColumnExtra {
pub auto_increment: bool,
pub on_update_current_timestamp: bool,
pub collation: Option<String>,
pub comment: Option<String>,
pub generation_expression: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexDef {
pub name: String,
pub columns: Vec<String>,
pub unique: bool,
pub is_primary_key: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForeignKeyDef {
pub name: String,
pub columns: Vec<String>,
pub referenced_schema: Option<String>,
pub referenced_table: String,
pub referenced_columns: Vec<String>,
pub on_delete: ReferentialAction,
pub on_update: ReferentialAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferentialAction {
NoAction,
Restrict,
Cascade,
SetNull,
SetDefault,
}
impl ReferentialAction {
fn parse(action: &str) -> Self {
match action.to_ascii_uppercase().as_str() {
"CASCADE" => Self::Cascade,
"SET NULL" => Self::SetNull,
"SET DEFAULT" => Self::SetDefault,
"RESTRICT" => Self::Restrict,
_ => Self::NoAction,
}
}
pub fn label(self) -> &'static str {
match self {
Self::NoAction => "NO ACTION",
Self::Restrict => "RESTRICT",
Self::Cascade => "CASCADE",
Self::SetNull => "SET NULL",
Self::SetDefault => "SET DEFAULT",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TableSchema {
pub columns: Vec<ColumnDef>,
pub indexes: Vec<IndexDef>,
pub foreign_keys: Vec<ForeignKeyDef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredDefinition {
pub name: String,
pub sql: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RebuildSource {
pub table_sql: String,
pub indexes: Vec<StoredDefinition>,
pub triggers: Vec<StoredDefinition>,
pub views: Vec<StoredDefinition>,
}
impl Connection {
pub async fn table_schema(&self, object: &DatabaseObject) -> Result<TableSchema> {
let engine = self.config.engine;
let schema = object.schema.as_deref().unwrap_or("public");
let table = object.name.as_str();
let (columns_sql, indexes_sql, foreign_keys_sql, primary_key_sql) = match engine {
Engine::Postgres => (
postgres::columns_sql(schema, table),
postgres::indexes_sql(schema, table),
postgres::foreign_keys_sql(schema, table),
postgres::primary_key_sql(schema, table),
),
Engine::MySql => (
mysql::columns_sql(table),
mysql::indexes_sql(table),
mysql::foreign_keys_sql(table),
mysql::primary_key_sql(table),
),
Engine::Sqlite => (
sqlite::columns_sql(table),
sqlite::indexes_sql(table),
sqlite::foreign_keys_sql(table),
sqlite::primary_key_sql(table),
),
};
let columns_result = self.run_query(&columns_sql).await?;
let indexes_result = self.run_query(&indexes_sql).await?;
let foreign_keys_result = self.run_query(&foreign_keys_sql).await?;
let primary_key_result = self.run_query(&primary_key_sql).await?;
let pk_columns: Vec<String> = primary_key_result
.rows
.iter()
.filter_map(|row| row.first().cloned().flatten())
.collect();
let mut columns = parse_columns(&columns_result);
for column in &mut columns {
column.is_primary_key = pk_columns.contains(&column.name);
}
Ok(TableSchema {
columns,
indexes: parse_indexes(&indexes_result),
foreign_keys: parse_foreign_keys(&foreign_keys_result),
})
}
pub async fn rebuild_source(&self, object: &DatabaseObject) -> Result<RebuildSource> {
if self.config.engine != Engine::Sqlite {
anyhow::bail!("a rebuild source is only read on SQLite");
}
let result = self.run_query(&sqlite::master_sql(&object.name)).await?;
let mut source = RebuildSource::default();
for row in &result.rows {
let kind = text(row, 0);
let name = text(row, 1);
let sql = text(row, 2);
let definition = || StoredDefinition {
name: name.clone(),
sql: sql.clone(),
};
match kind.as_str() {
"table" => source.table_sql = sql,
"index" => source.indexes.push(definition()),
"trigger" => source.triggers.push(definition()),
"view" => source.views.push(definition()),
_ => {}
}
}
Ok(source)
}
pub async fn foreign_keys(&self, object: &DatabaseObject) -> Result<Vec<ForeignKeyDef>> {
let engine = self.config.engine;
let schema = object.schema.as_deref().unwrap_or("public");
let table = object.name.as_str();
let sql = match engine {
Engine::Postgres => postgres::foreign_keys_sql(schema, table),
Engine::MySql => mysql::foreign_keys_sql(table),
Engine::Sqlite => sqlite::foreign_keys_sql(table),
};
let result = self.run_query(&sql).await?;
Ok(parse_foreign_keys(&result))
}
}
fn text(row: &[Cell], index: usize) -> String {
text_opt(row, index).unwrap_or_default()
}
fn text_opt(row: &[Cell], index: usize) -> Option<String> {
row.get(index).cloned().flatten()
}
fn flag(row: &[Cell], index: usize) -> bool {
text_opt(row, index).as_deref() == Some("1")
}
fn split_columns(joined: &str) -> Vec<String> {
joined
.split(',')
.filter(|part| !part.is_empty())
.map(str::to_string)
.collect()
}
fn parse_columns(result: &QueryResult) -> Vec<ColumnDef> {
result
.rows
.iter()
.map(|row| {
let extra = text(row, 4).to_ascii_lowercase();
ColumnDef {
name: text(row, 0),
type_name: text(row, 1),
nullable: flag(row, 2),
default: text_opt(row, 3),
is_primary_key: false,
mysql_extra: MySqlColumnExtra {
auto_increment: extra.contains("auto_increment"),
on_update_current_timestamp: extra.contains("on update current_timestamp"),
collation: text_opt(row, 5),
comment: text_opt(row, 6).filter(|comment| !comment.is_empty()),
generation_expression: text_opt(row, 7)
.filter(|expression| !expression.is_empty()),
},
}
})
.collect()
}
fn parse_indexes(result: &QueryResult) -> Vec<IndexDef> {
result
.rows
.iter()
.map(|row| IndexDef {
name: text(row, 0),
columns: split_columns(&text(row, 1)),
unique: flag(row, 2),
is_primary_key: flag(row, 3),
})
.collect()
}
fn parse_foreign_keys(result: &QueryResult) -> Vec<ForeignKeyDef> {
result
.rows
.iter()
.enumerate()
.map(|(position, row)| ForeignKeyDef {
name: text_opt(row, 0).unwrap_or_else(|| format!("fk_{}", position + 1)),
columns: split_columns(&text(row, 1)),
referenced_schema: text_opt(row, 2),
referenced_table: text(row, 3),
referenced_columns: split_columns(&text(row, 4)),
on_delete: ReferentialAction::parse(&text(row, 5)),
on_update: ReferentialAction::parse(&text(row, 6)),
})
.collect()
}