use crate::storage::Error;
use change_case::snake_case;
use futures_util::TryStreamExt;
use sqlx::{Pool, Postgres, Row};
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Clone, Debug)]
pub(crate) struct Field {
pub column_name: String,
pub data_type: String,
#[allow(unused)]
pub is_nullable: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct ForeignKey {
pub table_name: String,
#[allow(unused)]
pub column_name: String,
pub foreign_table_name: String,
#[allow(unused)]
pub foreign_column_name: String,
}
#[derive(Clone, Debug)]
pub(crate) struct JoinTable {
#[allow(unused)]
pub table_name: String,
pub foreign_keys: Vec<ForeignKey>,
}
#[derive(Clone, Debug)]
pub(crate) struct Table {
pub name: String,
pub fields: Vec<Field>,
pub fields_by_name: HashMap<String, Field>,
pub primary_key: Vec<String>,
}
pub(crate) async fn load_schema(pool: &Pool<Postgres>) -> Result<HashMap<String, Table>, Error> {
let query = "
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
";
let mut rows = sqlx::query(query).fetch(pool);
let mut tables_by_name: HashMap<String, Table> = HashMap::new();
while let Some(row) = rows.try_next().await? {
let table_name: String = row.get(0);
let column_name: String = row.get(1);
let data_type: String = row.get(2);
let is_nullable: bool = row.get::<&str, _>(3) == "YES";
let column_name_snake_case = snake_case(&column_name);
let field = Field {
column_name,
data_type,
is_nullable,
};
if let Some(table) = tables_by_name.get_mut(&table_name) {
table.fields.push(field.clone());
table.fields_by_name.insert(column_name_snake_case, field);
} else {
let mut fields_by_name = HashMap::new();
fields_by_name.insert(column_name_snake_case, field.clone());
let table = Table {
name: table_name.clone(),
fields: vec![field],
fields_by_name,
primary_key: vec![],
};
tables_by_name.insert(table_name, table);
}
}
let query = "
SELECT tc.table_name, c.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
JOIN information_schema.columns AS c
ON c.table_schema = tc.constraint_schema
AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
WHERE constraint_type = 'PRIMARY KEY'
";
let mut rows = sqlx::query(query).fetch(pool);
while let Some(row) = rows.try_next().await? {
let table_name: String = row.get(0);
let column_name: String = row.get(1);
let column_name_snake_case = snake_case(&column_name);
if let Some(table) = tables_by_name.get_mut(&table_name) {
table.primary_key.push(column_name_snake_case);
}
}
Ok(tables_by_name)
}
pub(crate) async fn load_foreign_keys(pool: &Pool<Postgres>) -> Result<Vec<ForeignKey>, Error> {
let query = "
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
";
let mut rows = sqlx::query(query).fetch(pool);
let mut foreign_keys = vec![];
while let Some(row) = rows.try_next().await? {
let table_name: String = row.get(0);
let column_name: String = row.get(1);
let foreign_table_name: String = row.get(2);
let foreign_column_name: String = row.get(3);
let foreign_key = ForeignKey {
table_name,
column_name,
foreign_table_name,
foreign_column_name,
};
foreign_keys.push(foreign_key);
}
Ok(foreign_keys)
}
pub(crate) fn find_join_tables(
tables_by_name: &HashMap<String, Table>,
foreign_keys: &[ForeignKey],
) -> HashMap<String, JoinTable> {
let mut join_tables_by_name: HashMap<String, JoinTable> = HashMap::new();
for fk in foreign_keys.iter().filter_map(|fk| {
tables_by_name
.get(&fk.table_name)
.filter(|&table| table.primary_key.len() == 2)
.map(|_table| fk)
}) {
if let Some(join_table) = join_tables_by_name.get_mut(&fk.table_name) {
join_table.foreign_keys.push(fk.clone());
} else {
let join_table = crate::storage::config_store::adapters::postgres::schema::JoinTable {
table_name: fk.table_name.clone(),
foreign_keys: vec![fk.clone()],
};
join_tables_by_name.insert(fk.table_name.clone(), join_table);
}
}
join_tables_by_name
}