use std::{future::Future, sync::Arc};
use async_trait::async_trait;
use fraiseql_error::{FraiseQLError, Result};
use super::{
types::{DatabaseType, JsonbValue, PoolMetrics},
where_clause::WhereClause,
};
use crate::types::sql_hints::{OrderByClause, SqlProjectionHint};
#[derive(Debug, Clone)]
pub struct RelayPageResult {
pub rows: Vec<JsonbValue>,
pub total_count: Option<u64>,
}
#[async_trait]
pub trait DatabaseAdapter: Send + Sync {
async fn execute_where_query(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
) -> Result<Vec<JsonbValue>>;
async fn execute_with_projection(
&self,
view: &str,
projection: Option<&SqlProjectionHint>,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
) -> Result<Vec<JsonbValue>>;
async fn execute_where_query_arc(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_where_query(view, where_clause, limit, offset, order_by, session_vars)
.await
.map(Arc::new)
}
async fn execute_with_projection_arc(
&self,
view: &str,
projection: Option<&SqlProjectionHint>,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
session_vars: &[(&str, &str)],
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_with_projection(view, projection, where_clause, limit, offset, order_by, session_vars)
.await
.map(Arc::new)
}
fn database_type(&self) -> DatabaseType;
async fn health_check(&self) -> Result<()>;
fn pool_metrics(&self) -> PoolMetrics;
async fn execute_raw_query(
&self,
sql: &str,
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
async fn execute_row_query(
&self,
view_name: &str,
columns: &[crate::types::ColumnSpec],
where_sql: Option<&str>,
order_by: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
use crate::types::ColumnValue;
let mut sql = format!("SELECT * FROM \"{view_name}\"");
if let Some(w) = where_sql {
sql.push_str(" WHERE ");
sql.push_str(w);
}
if let Some(ob) = order_by {
sql.push_str(" ORDER BY ");
sql.push_str(ob);
}
if let Some(l) = limit {
use std::fmt::Write;
let _ = write!(sql, " LIMIT {l}");
}
if let Some(o) = offset {
use std::fmt::Write;
let _ = write!(sql, " OFFSET {o}");
}
let results = self.execute_raw_query(&sql).await?;
Ok(results
.iter()
.map(|row| {
columns
.iter()
.map(|col| {
row.get(&col.name).map_or(ColumnValue::Null, |v| match v {
serde_json::Value::Null => ColumnValue::Null,
serde_json::Value::Bool(b) => ColumnValue::Boolean(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
ColumnValue::Int64(i)
} else if let Some(f) = n.as_f64() {
ColumnValue::Float64(f)
} else {
ColumnValue::Text(n.to_string())
}
},
serde_json::Value::String(s) => ColumnValue::Text(s.clone()),
other => ColumnValue::Json(other.to_string()),
})
})
.collect()
})
.collect())
}
async fn execute_parameterized_aggregate(
&self,
sql: &str,
params: &[serde_json::Value],
session_vars: &[(&str, &str)],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
async fn execute_function_call(
&self,
function_name: &str,
_args: &[serde_json::Value],
_session_vars: &[(&str, &str)],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
Err(FraiseQLError::Unsupported {
message: format!(
"Mutations via function calls are not supported by this adapter. \
Function '{function_name}' cannot be executed. \
Use PostgreSQL, MySQL, or SQL Server for mutation support."
),
})
}
fn supports_mutations(&self) -> bool {
true
}
async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
Ok(())
}
async fn invalidate_views(&self, _views: &[String]) -> Result<u64> {
Ok(0)
}
async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
Ok(0)
}
async fn invalidate_list_queries(&self, views: &[String]) -> Result<u64> {
self.invalidate_views(views).await
}
fn capabilities(&self) -> DatabaseCapabilities {
DatabaseCapabilities::from_database_type(self.database_type())
}
async fn explain_query(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> Result<serde_json::Value> {
Err(fraiseql_error::FraiseQLError::Unsupported {
message: "EXPLAIN not available for this database adapter".to_string(),
})
}
async fn explain_where_query(
&self,
_view: &str,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
) -> Result<serde_json::Value> {
Err(fraiseql_error::FraiseQLError::Unsupported {
message: "EXPLAIN ANALYZE is not available for this database adapter. \
Only PostgreSQL supports explain_where_query."
.to_string(),
})
}
fn mutation_strategy(&self) -> MutationStrategy {
MutationStrategy::FunctionCall
}
async fn set_session_variables(&self, _variables: &[(&str, &str)]) -> Result<()> {
Ok(())
}
async fn execute_direct_mutation(
&self,
_ctx: &DirectMutationContext<'_>,
) -> Result<Vec<serde_json::Value>> {
Err(FraiseQLError::Unsupported {
message: "Direct SQL mutations are not supported by this adapter. \
Use execute_function_call for stored-procedure mutations."
.to_string(),
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct DatabaseCapabilities {
pub database_type: DatabaseType,
pub supports_locale_collation: bool,
pub requires_custom_collation: bool,
pub recommended_collation: Option<&'static str>,
}
impl DatabaseCapabilities {
#[must_use]
pub const fn from_database_type(db_type: DatabaseType) -> Self {
match db_type {
DatabaseType::PostgreSQL => Self {
database_type: db_type,
supports_locale_collation: true,
requires_custom_collation: false,
recommended_collation: Some("icu"),
},
DatabaseType::MySQL => Self {
database_type: db_type,
supports_locale_collation: false,
requires_custom_collation: false,
recommended_collation: Some("utf8mb4_unicode_ci"),
},
DatabaseType::SQLite => Self {
database_type: db_type,
supports_locale_collation: false,
requires_custom_collation: true,
recommended_collation: Some("NOCASE"),
},
DatabaseType::SQLServer => Self {
database_type: db_type,
supports_locale_collation: true,
requires_custom_collation: false,
recommended_collation: Some("Latin1_General_100_CI_AI_SC_UTF8"),
},
}
}
#[must_use]
pub const fn collation_strategy(&self) -> &'static str {
match self.database_type {
DatabaseType::PostgreSQL => "ICU collations (locale-specific)",
DatabaseType::MySQL => "UTF8MB4 collations (general)",
DatabaseType::SQLite => "NOCASE (limited)",
DatabaseType::SQLServer => "Language-specific collations",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MutationStrategy {
FunctionCall,
DirectSql,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DirectMutationOp {
Insert,
Update,
Delete,
}
#[derive(Debug)]
pub struct DirectMutationContext<'a> {
pub operation: DirectMutationOp,
pub table: &'a str,
pub columns: &'a [String],
pub values: &'a [serde_json::Value],
pub inject_columns: &'a [String],
pub return_type: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CursorValue {
Int64(i64),
Uuid(String),
}
pub trait RelayDatabaseAdapter: DatabaseAdapter {
fn execute_relay_page<'a>(
&'a self,
view: &'a str,
cursor_column: &'a str,
after: Option<CursorValue>,
before: Option<CursorValue>,
limit: u32,
forward: bool,
where_clause: Option<&'a WhereClause>,
order_by: Option<&'a [OrderByClause]>,
include_total_count: bool,
session_vars: &'a [(&'a str, &'a str)],
) -> impl Future<Output = Result<RelayPageResult>> + Send + 'a;
}
pub trait SupportsMutations: DatabaseAdapter {}
pub type BoxDatabaseAdapter = Box<dyn DatabaseAdapter>;
pub type ArcDatabaseAdapter = std::sync::Arc<dyn DatabaseAdapter>;
#[cfg(test)]
mod tests {
#[allow(clippy::unwrap_used)] #[test]
fn database_adapter_is_send_sync() {
fn assert_send_sync<T: Send + Sync + ?Sized>() {}
assert_send_sync::<dyn super::DatabaseAdapter>();
}
}