use larust_core::AppError;
use larust_orm::Backend;
use sqlx::any::AnyTypeInfoKind;
use sqlx::Row;
#[derive(Debug, Clone)]
pub struct ColumnInfo {
pub name: String,
pub not_null: bool,
pub kind: AnyTypeInfoKind,
}
fn internal<E: std::error::Error + Send + Sync + 'static>(error: E) -> AppError {
AppError::Internal(Box::new(error))
}
pub async fn list_tables() -> Result<Vec<String>, AppError> {
larust_orm::table_names().await
}
pub async fn list_indexes(table: &str) -> Result<Vec<serde_json::Value>, AppError> {
let pool = larust_orm::pool()?;
let rows = match larust_orm::backend() {
Backend::Sqlite => {
let sql = format!("PRAGMA index_list(\"{table}\")");
sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
}
Backend::MySql => {
let sql = format!("SHOW INDEX FROM \"{table}\"");
sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
}
backend @ Backend::Postgres => {
let sql = format!(
"SELECT indexname, indexdef FROM pg_indexes WHERE tablename = {}",
larust_orm::placeholder(backend, 1)
);
sqlx::query(&sql)
.bind(table)
.fetch_all(pool)
.await
.map_err(internal)?
}
};
Ok(rows.iter().map(crate::sql::codec::row_to_json).collect())
}
pub async fn list_foreign_keys(table: &str) -> Result<Vec<serde_json::Value>, AppError> {
let pool = larust_orm::pool()?;
let rows = match larust_orm::backend() {
Backend::Sqlite => {
let sql = format!("PRAGMA foreign_key_list(\"{table}\")");
sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?
}
Backend::MySql => {
let sql = "SELECT column_name, referenced_table_name, referenced_column_name, \
constraint_name FROM information_schema.key_column_usage \
WHERE table_schema = DATABASE() AND table_name = ? \
AND referenced_table_name IS NOT NULL";
sqlx::query(sql)
.bind(table)
.fetch_all(pool)
.await
.map_err(internal)?
}
Backend::Postgres => {
let sql = "SELECT kcu.column_name, ccu.table_name AS referenced_table_name, \
ccu.column_name AS referenced_column_name, tc.constraint_name \
FROM information_schema.table_constraints tc \
JOIN information_schema.key_column_usage kcu \
ON tc.constraint_name = kcu.constraint_name \
AND tc.table_schema = kcu.table_schema \
JOIN information_schema.constraint_column_usage ccu \
ON tc.constraint_name = ccu.constraint_name \
AND tc.table_schema = ccu.table_schema \
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1";
sqlx::query(sql)
.bind(table)
.fetch_all(pool)
.await
.map_err(internal)?
}
};
Ok(rows.iter().map(crate::sql::codec::row_to_json).collect())
}
pub async fn table_columns(table: &str) -> Result<Vec<ColumnInfo>, AppError> {
let pool = larust_orm::pool()?;
match larust_orm::backend() {
Backend::Sqlite => {
let sql = format!("PRAGMA table_info(\"{table}\")");
let rows = sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?;
Ok(rows
.iter()
.map(|row| {
let name: String = row.get("name");
let type_text: String = row.get("type");
let notnull: i64 = row.get("notnull");
ColumnInfo {
name,
not_null: notnull != 0,
kind: infer_kind(&type_text),
}
})
.collect())
}
backend @ (Backend::MySql | Backend::Postgres) => {
let sql = format!(
"SELECT column_name, data_type, is_nullable FROM information_schema.columns \
WHERE table_name = {} ORDER BY ordinal_position",
larust_orm::placeholder(backend, 1)
);
let rows = sqlx::query(&sql)
.bind(table)
.fetch_all(pool)
.await
.map_err(internal)?;
Ok(rows
.iter()
.map(|row| {
let name: String = row.get("column_name");
let type_text: String = row.get("data_type");
let is_nullable: String = row.get("is_nullable");
ColumnInfo {
name,
not_null: is_nullable != "YES",
kind: infer_kind(&type_text),
}
})
.collect())
}
}
}
pub async fn primary_key_columns(table: &str) -> Result<Vec<String>, AppError> {
let pool = larust_orm::pool()?;
match larust_orm::backend() {
Backend::Sqlite => {
let sql = format!("PRAGMA table_info(\"{table}\")");
let rows = sqlx::query(&sql).fetch_all(pool).await.map_err(internal)?;
let mut pk: Vec<(i64, String)> = rows
.iter()
.filter_map(|row| {
let ordinal: i64 = row.get("pk");
if ordinal == 0 {
return None;
}
let name: String = row.get("name");
Some((ordinal, name))
})
.collect();
pk.sort_by_key(|(ordinal, _)| *ordinal);
Ok(pk.into_iter().map(|(_, name)| name).collect())
}
backend @ (Backend::MySql | Backend::Postgres) => {
let sql = format!(
"SELECT kcu.column_name FROM information_schema.key_column_usage kcu \
JOIN information_schema.table_constraints tc \
ON kcu.constraint_name = tc.constraint_name \
AND kcu.table_schema = tc.table_schema \
WHERE tc.constraint_type = 'PRIMARY KEY' AND kcu.table_name = {} \
ORDER BY kcu.ordinal_position",
larust_orm::placeholder(backend, 1)
);
let rows = sqlx::query(&sql)
.bind(table)
.fetch_all(pool)
.await
.map_err(internal)?;
Ok(rows
.iter()
.map(|row| row.get::<String, _>("column_name"))
.collect())
}
}
}
fn infer_kind(declared_type: &str) -> AnyTypeInfoKind {
let upper = declared_type.to_ascii_uppercase();
if upper.contains("BOOL") {
AnyTypeInfoKind::Bool
} else if upper.contains("BLOB") || upper.contains("BINARY") || upper.contains("BYTEA") {
AnyTypeInfoKind::Blob
} else if upper.contains("DOUBLE")
|| upper.contains("REAL")
|| upper.contains("FLOA")
|| upper.contains("DEC")
|| upper.contains("NUMERIC")
{
AnyTypeInfoKind::Double
} else if upper.contains("INT") {
AnyTypeInfoKind::BigInt
} else {
AnyTypeInfoKind::Text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn infer_kind_recognizes_common_sqlite_and_information_schema_type_names() {
assert_eq!(infer_kind("INTEGER"), AnyTypeInfoKind::BigInt);
assert_eq!(infer_kind("integer"), AnyTypeInfoKind::BigInt);
assert_eq!(infer_kind("BIGINT"), AnyTypeInfoKind::BigInt);
assert_eq!(infer_kind("TEXT"), AnyTypeInfoKind::Text);
assert_eq!(infer_kind("character varying"), AnyTypeInfoKind::Text);
assert_eq!(infer_kind("VARCHAR(255)"), AnyTypeInfoKind::Text);
assert_eq!(infer_kind("REAL"), AnyTypeInfoKind::Double);
assert_eq!(infer_kind("DOUBLE PRECISION"), AnyTypeInfoKind::Double);
assert_eq!(infer_kind("NUMERIC(10,2)"), AnyTypeInfoKind::Double);
assert_eq!(infer_kind("BOOLEAN"), AnyTypeInfoKind::Bool);
assert_eq!(infer_kind("BLOB"), AnyTypeInfoKind::Blob);
assert_eq!(infer_kind("bytea"), AnyTypeInfoKind::Blob);
assert_eq!(infer_kind("some unknown type"), AnyTypeInfoKind::Text);
}
}