mod mysql;
mod pg;
use crate::{config::DatabaseConfig, error::AppError};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlparser::{ast, dialect::GenericDialect, parser::Parser};
use sqlx::{MySqlPool, PgPool};
use std::{cmp::min, convert::Infallible, str::FromStr, time::Duration};
const DEFAULT_LIMIT: usize = 500;
const MAX_LIMIT: usize = 5000;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
pub enum DatabaseType {
Postgres,
Mysql,
}
#[derive(Debug)]
pub struct PgPoolHandler(PgPool);
#[derive(Debug)]
pub struct MySqlPoolHandler(MySqlPool);
#[derive(Debug)]
pub enum DbPool {
Postgres(PgPoolHandler),
MySql(MySqlPoolHandler),
}
pub trait PoolHandler: Sized {
async fn try_new(db_config: &DatabaseConfig) -> Result<Self, AppError>;
async fn list_tables(&self) -> Result<Vec<TableInfo>, AppError>;
async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema, AppError>;
async fn sanitize_query(&self, query: &str, limit: usize) -> Result<String, AppError> {
let dialect = GenericDialect {};
let ast = Parser::parse_sql(&dialect, query)
.map_err(|e| AppError::BadRequest(format!("SQL parsing error: {}", e)))?;
if ast.len() != 1 {
return Err(AppError::BadRequest(
"Only single SQL statements are allowed".to_string(),
));
}
let mut stmt = ast.into_iter().next().unwrap();
let has_limit = match stmt {
ast::Statement::Query(ref mut query) => {
match &*query.body {
ast::SetExpr::Select(_)
| ast::SetExpr::Values(_)
| ast::SetExpr::Query(_)
| ast::SetExpr::Table(_) => {
}
_ => {
return Err(AppError::BadRequest(
"Only SELECT-like queries are allowed.".to_string(),
));
}
}
match &mut query.limit {
Some(ast::Expr::Value(ast::ValueWithSpan {
value: ast::Value::Number(s, _),
..
})) => {
let existing_limit = s.parse::<usize>().unwrap_or(0);
if existing_limit < limit {
} else {
*s = min(existing_limit, MAX_LIMIT).to_string();
}
true
}
_ => false,
}
}
_ => {
return Err(AppError::BadRequest(
"Only SELECT queries are allowed".to_string(),
));
}
};
let mut sql = stmt.to_string();
if !has_limit {
sql = format!("{} LIMIT {}", sql, limit);
}
Ok(sql)
}
async fn execute_query(
&self,
query: &str,
limit: Option<usize>,
) -> Result<QueryResult, AppError>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseInfo {
pub name: String,
#[serde(rename = "type")]
pub db_type: String, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TableType {
Table,
View,
MaterializedView,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct TableInfo {
pub name: String,
#[sqlx(rename = "type", try_from = "String")]
#[serde(rename = "type")]
pub table_type: TableType, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ColumnType {
SmallInt,
Integer,
BigInt,
Decimal,
Numeric,
Real,
DoublePrecision,
Char,
Varchar,
Text,
Bytea,
Boolean,
Date,
Time,
Timestamp,
TimestampTz,
Interval,
Json,
Jsonb,
Inet,
Cidr,
MacAddr,
Uuid,
Point,
Line,
Lseg,
Box,
Path,
Polygon,
Circle,
Array,
Int4Range,
Int8Range,
NumRange,
TsRange,
TstzRange,
DateRange,
Bit,
Varbit,
TsVector,
TsQuery,
Xml,
Money,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnInfo {
pub name: String,
pub data_type: ColumnType,
pub is_nullable: bool,
#[serde(default)]
pub is_pk: bool,
#[serde(default)]
pub is_unique: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fk_table: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fk_column: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchema {
pub table_name: String,
pub columns: Vec<ColumnInfo>,
}
#[derive(Debug, Serialize)]
pub struct QueryResult {
pub data: Value,
pub execution_time: Duration,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<Value>,
}
#[derive(sqlx::FromRow)]
pub struct JsonResult {
pub data: Value,
}
impl FromStr for TableType {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"table" => Ok(TableType::Table),
"view" => Ok(TableType::View),
"materialized_view" => Ok(TableType::MaterializedView),
_ => unreachable!(),
}
}
}
impl From<String> for TableType {
fn from(s: String) -> Self {
TableType::from_str(&s).unwrap()
}
}
impl TableType {
pub fn as_str(&self) -> &str {
match self {
TableType::Table => "table",
TableType::View => "view",
TableType::MaterializedView => "materialized_view",
}
}
}
impl FromStr for ColumnType {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"smallint" => Ok(ColumnType::SmallInt),
"integer" => Ok(ColumnType::Integer),
"bigint" => Ok(ColumnType::BigInt),
"decimal" => Ok(ColumnType::Decimal),
"numeric" => Ok(ColumnType::Numeric),
"real" => Ok(ColumnType::Real),
"double precision" => Ok(ColumnType::DoublePrecision),
"money" => Ok(ColumnType::Money),
"text" => Ok(ColumnType::Text),
"char" => Ok(ColumnType::Char),
"character" => Ok(ColumnType::Char),
"varchar" => Ok(ColumnType::Varchar),
"character varying" => Ok(ColumnType::Varchar),
"boolean" => Ok(ColumnType::Boolean),
"json" => Ok(ColumnType::Json),
"jsonb" => Ok(ColumnType::Jsonb),
"bytea" => Ok(ColumnType::Bytea),
"uuid" => Ok(ColumnType::Uuid),
"inet" => Ok(ColumnType::Inet),
"cidr" => Ok(ColumnType::Cidr),
"macaddr" => Ok(ColumnType::MacAddr),
"point" => Ok(ColumnType::Point),
"line" => Ok(ColumnType::Line),
"lseg" => Ok(ColumnType::Lseg),
"box" => Ok(ColumnType::Box),
"path" => Ok(ColumnType::Path),
"polygon" => Ok(ColumnType::Polygon),
"circle" => Ok(ColumnType::Circle),
"array" => Ok(ColumnType::Array),
"int4range" => Ok(ColumnType::Int4Range),
"int8range" => Ok(ColumnType::Int8Range),
"numrange" => Ok(ColumnType::NumRange),
"tsrange" => Ok(ColumnType::TsRange),
"tstzrange" => Ok(ColumnType::TstzRange),
"date" => Ok(ColumnType::Date),
"datetime" => Ok(ColumnType::Timestamp),
"time" => Ok(ColumnType::Time),
"timestamp" => Ok(ColumnType::Timestamp),
"timestamp with time zone" => Ok(ColumnType::TimestampTz),
"interval" => Ok(ColumnType::Interval),
"daterange" => Ok(ColumnType::DateRange),
"bit" => Ok(ColumnType::Bit),
"varbit" => Ok(ColumnType::Varbit),
"tsvector" => Ok(ColumnType::TsVector),
"tsquery" => Ok(ColumnType::TsQuery),
"xml" => Ok(ColumnType::Xml),
v => Ok(ColumnType::Other(v.to_string())),
}
}
}
impl From<String> for ColumnType {
fn from(s: String) -> Self {
ColumnType::from_str(&s).unwrap_or_else(|_| {
tracing::warn!("Unsupported database type string encountered: {}", s);
unreachable!("unsupported type: {}", s)
})
}
}
impl sqlx::Type<sqlx::Postgres> for ColumnType {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("TEXT") }
}
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for ColumnType {
fn decode(
value: sqlx::postgres::PgValueRef<'r>,
) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let s = <String as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
Ok(ColumnType::from_str(&s).map_err(|_| "Invalid ColumnType string")?)
}
}
impl PoolHandler for DbPool {
async fn try_new(db_config: &DatabaseConfig) -> Result<Self, AppError> {
match db_config.db_type {
DatabaseType::Postgres => {
let pool = PgPoolHandler::try_new(db_config).await?;
Ok(DbPool::Postgres(pool))
}
DatabaseType::Mysql => {
let pool = MySqlPoolHandler::try_new(db_config).await?;
Ok(DbPool::MySql(pool))
}
#[allow(unreachable_patterns)]
_ => Err(AppError::UnsupportedDatabaseType(
db_config.db_type.to_string(),
)),
}
}
async fn list_tables(&self) -> Result<Vec<TableInfo>, AppError> {
match self {
DbPool::Postgres(pg_pool) => pg_pool.list_tables().await,
DbPool::MySql(mysql_pool) => mysql_pool.list_tables().await,
}
}
async fn get_table_schema(&self, table_name: &str) -> Result<TableSchema, AppError> {
match self {
DbPool::Postgres(pg_pool) => pg_pool.get_table_schema(table_name).await,
DbPool::MySql(mysql_pool) => mysql_pool.get_table_schema(table_name).await,
}
}
async fn sanitize_query(&self, query: &str, limit: usize) -> Result<String, AppError> {
match self {
DbPool::Postgres(pg_pool) => pg_pool.sanitize_query(query, limit).await,
DbPool::MySql(mysql_pool) => mysql_pool.sanitize_query(query, limit).await,
}
}
async fn execute_query(
&self,
query: &str,
limit: Option<usize>,
) -> Result<QueryResult, AppError> {
match self {
DbPool::Postgres(pg_pool) => pg_pool.execute_query(query, limit).await,
DbPool::MySql(mysql_pool) => mysql_pool.execute_query(query, limit).await,
}
}
}