mod adapter_types;
mod composed_read;
mod mutations;
mod relay;
use std::sync::Arc;
pub use adapter_types::*;
use async_trait::async_trait;
pub use composed_read::{
COMPOSED_DOCUMENT_KEY, COMPOSED_EMBEDS_KEY, ComposedEmbed, ComposedKeyset, ComposedLevel,
EmbedShape, EmbedSource, LevelKeys, composed_read_unsupported,
};
use fraiseql_error::{FraiseQLError, Result};
pub use mutations::{WriteMode, WriteRequest, Writer};
pub use relay::RelayDatabaseAdapter;
use crate::{
types::{
DatabaseType, JsonbValue, PoolMetrics, ReadRouting,
sql_hints::{OrderByClause, SqlProjectionHint},
},
where_clause::WhereClause,
};
pub type MutationRowGate<'a> = &'a (
dyn Fn(&[std::collections::HashMap<String, serde_json::Value>]) -> Result<()> + Send + Sync
);
#[derive(Debug, Clone, Copy)]
pub struct ChangeLogWrite<'a> {
pub object_type: &'a str,
pub modification_type: &'a str,
pub tenant_id: Option<uuid::Uuid>,
pub trace_id: Option<&'a str>,
pub schema_version: Option<&'a str>,
pub trace_context: Option<&'a str>,
pub actor_type: Option<&'a str>,
pub acting_for: Option<uuid::Uuid>,
pub transport: Option<&'a str>,
pub pre_image: bool,
}
impl<'a> ChangeLogWrite<'a> {
#[must_use]
pub const fn new(object_type: &'a str, modification_type: &'a str) -> Self {
Self {
object_type,
modification_type,
tenant_id: None,
trace_id: None,
schema_version: None,
trace_context: None,
actor_type: None,
acting_for: None,
transport: None,
pre_image: false,
}
}
#[must_use]
pub const fn with_tenant_id(mut self, tenant_id: Option<uuid::Uuid>) -> Self {
self.tenant_id = tenant_id;
self
}
#[must_use]
pub const fn with_trace_id(mut self, trace_id: Option<&'a str>) -> Self {
self.trace_id = trace_id;
self
}
#[must_use]
pub const fn with_schema_version(mut self, schema_version: Option<&'a str>) -> Self {
self.schema_version = schema_version;
self
}
#[must_use]
pub const fn with_trace_context(mut self, trace_context: Option<&'a str>) -> Self {
self.trace_context = trace_context;
self
}
#[must_use]
pub const fn with_actor_type(mut self, actor_type: Option<&'a str>) -> Self {
self.actor_type = actor_type;
self
}
#[must_use]
pub const fn with_acting_for(mut self, acting_for: Option<uuid::Uuid>) -> Self {
self.acting_for = acting_for;
self
}
#[must_use]
pub const fn with_transport(mut self, transport: Option<&'a str>) -> Self {
self.transport = transport;
self
}
#[must_use]
pub const fn with_pre_image(mut self, pre_image: bool) -> Self {
self.pre_image = pre_image;
self
}
}
#[async_trait]
pub trait DatabaseAdapter: Send + Sync + 'static {
async fn execute_where_query(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
) -> 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]>,
) -> 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]>,
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_where_query(view, where_clause, limit, offset, order_by)
.await
.map(Arc::new)
}
async fn execute_with_projection_arc(
&self,
request: &ProjectionRequest<'_>,
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_with_projection(
request.view,
request.projection,
request.where_clause,
request.limit,
request.offset,
request.order_by,
)
.await
.map(Arc::new)
}
fn database_type(&self) -> DatabaseType;
fn serves_reads_from_standbys(&self) -> bool {
false
}
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>>> {
let sql = build_row_query_sql(view_name, where_sql, order_by, limit, offset);
let results = self.execute_raw_query(&sql).await?;
Ok(results.iter().map(|row| row_to_column_values(row, columns)).collect())
}
async fn execute_row_query_with_session(
&self,
view_name: &str,
columns: &[crate::types::ColumnSpec],
where_sql: Option<&str>,
order_by: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
_session_vars: &[(&str, &str)],
) -> Result<Vec<Vec<crate::types::ColumnValue>>> {
self.execute_row_query(view_name, columns, where_sql, order_by, limit, offset)
.await
}
async fn execute_parameterized_aggregate(
&self,
sql: &str,
params: &[serde_json::Value],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
async fn execute_parameterized_aggregate_with_session(
&self,
sql: &str,
params: &[serde_json::Value],
_session_vars: &[(&str, &str)],
_routing: ReadRouting,
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
self.execute_parameterized_aggregate(sql, params).await
}
fn supports_composed_reads(&self) -> bool {
false
}
async fn invalidate_views(&self, _views: &[crate::ViewName]) -> Result<u64> {
Ok(0)
}
async fn invalidate_by_entity(&self, _entity_type: &str, _entity_id: &str) -> Result<u64> {
Ok(0)
}
fn result_cache_stats(&self) -> Option<crate::ResultCacheStats> {
None
}
async fn clear_result_cache(&self) -> Result<Option<usize>> {
Ok(None)
}
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(),
})
}
async fn execute_where_query_arc_with_session(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
order_by: Option<&[OrderByClause]>,
_session_vars: &[(&str, &str)],
_routing: ReadRouting,
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await
}
async fn count_where_query(
&self,
view: &str,
where_clause: Option<&WhereClause>,
session_vars: &[(&str, &str)],
routing: ReadRouting,
) -> Result<u64> {
let rows = self
.execute_where_query_arc_with_session(
view,
where_clause,
None,
None,
None,
session_vars,
routing,
)
.await?;
Ok(rows.len() as u64)
}
async fn execute_with_projection_arc_with_session(
&self,
request: &ProjectionRequest<'_>,
_session_vars: &[(&str, &str)],
_routing: ReadRouting,
) -> Result<Arc<Vec<JsonbValue>>> {
self.execute_with_projection_arc(request).await
}
async fn stream_with_projection(
&self,
request: &ProjectionRequest<'_>,
session_vars: &[(&str, &str)],
routing: ReadRouting,
) -> Result<JsonbRowStream> {
let rows = self
.execute_with_projection_arc_with_session(request, session_vars, routing)
.await?;
let rows = Arc::try_unwrap(rows).unwrap_or_else(|shared| (*shared).clone());
Ok(Box::pin(futures::stream::iter(rows.into_iter().map(Ok))))
}
async fn execute_composed_with_session(
&self,
read: &ComposedLevel,
_session_vars: &[(&str, &str)],
_routing: ReadRouting,
) -> Result<Arc<Vec<JsonbValue>>> {
Err(composed_read_unsupported(&read.view))
}
async fn stream_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<ColumnRowStream> {
let rows = self
.execute_row_query(view_name, columns, where_sql, order_by, limit, offset)
.await?;
Ok(Box::pin(futures::stream::iter(rows.into_iter().map(Ok))))
}
async fn stream_row_query_with_session(
&self,
view_name: &str,
columns: &[crate::types::ColumnSpec],
where_sql: Option<&str>,
order_by: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
_session_vars: &[(&str, &str)],
) -> Result<ColumnRowStream> {
self.stream_row_query(view_name, columns, where_sql, order_by, limit, offset)
.await
}
async fn query_stats(&self, _limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
Ok(vec![])
}
async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
let stats = self.query_stats(1000).await?;
Ok(stats.into_iter().find(|e| e.query_id == id))
}
async fn reset_query_stats(&self) -> Result<()> {
Err(FraiseQLError::Unsupported {
message: "Query stats reset is not supported by this database adapter".to_string(),
})
}
fn on_schema_reload(&self) {}
async fn execute_admin_sql(&self, _request: &AdminSqlRequest) -> Result<AdminSqlOutcome> {
Err(FraiseQLError::Unsupported {
message: "Operator-supplied SQL execution is not supported by this database adapter."
.to_string(),
})
}
}
#[must_use]
pub fn build_row_query_sql(
view_name: &str,
where_sql: Option<&str>,
order_by: Option<&str>,
limit: Option<u32>,
offset: Option<u32>,
) -> String {
use std::fmt::Write as _;
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 {
let _ = write!(sql, " LIMIT {l}");
}
if let Some(o) = offset {
let _ = write!(sql, " OFFSET {o}");
}
sql
}
#[must_use]
pub fn row_to_column_values<S: std::hash::BuildHasher>(
row: &std::collections::HashMap<String, serde_json::Value, S>,
columns: &[crate::types::ColumnSpec],
) -> Vec<crate::types::ColumnValue> {
use crate::types::ColumnValue;
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()
}
#[cfg(test)]
mod tests;