use std::sync::Arc;
use async_trait::async_trait;
use bytes::BufMut as _;
use fraiseql_error::{FraiseQLError, Result};
use tokio_postgres::Row;
use super::{
PostgresAdapter, build_projection_select_sql, build_where_select_sql,
build_where_select_sql_ordered,
};
use crate::{
identifier::quote_postgres_identifier,
traits::{DatabaseAdapter, ProjectionRequest, SupportsMutations},
types::{
DatabaseType, JsonbValue, PoolMetrics, QueryParam,
sql_hints::{OrderByClause, SqlProjectionHint},
},
where_clause::WhereClause,
};
const PG_UNDEFINED_COLUMN: &str = "42703";
#[derive(Debug)]
enum FlexParam {
Null,
Text(String),
}
impl tokio_postgres::types::ToSql for FlexParam {
fn to_sql(
&self,
ty: &tokio_postgres::types::Type,
out: &mut bytes::BytesMut,
) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
{
use tokio_postgres::types::{IsNull, Type};
match self {
Self::Null => Ok(IsNull::Yes),
Self::Text(s) => {
if *ty == Type::JSONB {
out.put_u8(1);
out.extend_from_slice(s.as_bytes());
} else if *ty == Type::JSON {
out.extend_from_slice(s.as_bytes());
} else if *ty == Type::UUID {
let uuid = uuid::Uuid::parse_str(s)?;
out.extend_from_slice(uuid.as_bytes());
} else if *ty == Type::INT4 {
let n: i32 = s.parse()?;
out.put_i32(n);
} else if *ty == Type::INT8 {
let n: i64 = s.parse()?;
out.put_i64(n);
} else if *ty == Type::BOOL {
let b: bool = s.parse()?;
out.put_u8(u8::from(b));
} else {
out.extend_from_slice(s.as_bytes());
}
Ok(IsNull::No)
},
}
}
fn accepts(_ty: &tokio_postgres::types::Type) -> bool {
true
}
fn to_sql_checked(
&self,
ty: &tokio_postgres::types::Type,
out: &mut bytes::BytesMut,
) -> std::result::Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>>
{
self.to_sql(ty, out)
}
}
fn enrich_undefined_column_error(
err: FraiseQLError,
view: &str,
where_clause: Option<&WhereClause>,
) -> FraiseQLError {
let FraiseQLError::Database { ref sql_state, .. } = err else {
return err;
};
if sql_state.as_deref() != Some(PG_UNDEFINED_COLUMN) {
return err;
}
let native_cols: Vec<&str> =
where_clause.map(|wc| wc.native_column_names()).unwrap_or_default();
if native_cols.is_empty() {
return err;
}
FraiseQLError::Database {
message: format!(
"Column(s) {:?} referenced as native column(s) on `{view}` do not exist. \
These columns were auto-inferred from ID/UUID-typed query arguments. \
Either add the column(s) to the table/view, or set \
`native_columns = {{}}` explicitly in your schema to disable inference.",
native_cols,
),
sql_state: Some(PG_UNDEFINED_COLUMN.to_string()),
}
}
fn row_to_map(row: &Row) -> std::collections::HashMap<String, serde_json::Value> {
let mut map = std::collections::HashMap::new();
for (idx, column) in row.columns().iter().enumerate() {
let column_name = column.name().to_string();
let value: serde_json::Value = if let Ok(v) = row.try_get::<_, i16>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, i32>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, i64>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, f64>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, String>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, bool>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, rust_decimal::Decimal>(idx) {
let s = v.to_string();
serde_json::from_str::<serde_json::Value>(&s).unwrap_or(serde_json::Value::String(s))
} else if let Ok(v) = row.try_get::<_, uuid::Uuid>(idx) {
serde_json::json!(v.to_string())
} else if let Ok(v) = row.try_get::<_, chrono::DateTime<chrono::Utc>>(idx) {
serde_json::json!(v.to_rfc3339())
} else if let Ok(v) = row.try_get::<_, chrono::NaiveDateTime>(idx) {
serde_json::json!(v.to_string())
} else if let Ok(v) = row.try_get::<_, chrono::NaiveDate>(idx) {
serde_json::json!(v.to_string())
} else if let Ok(v) = row.try_get::<_, Vec<String>>(idx) {
serde_json::json!(v)
} else if let Ok(v) = row.try_get::<_, serde_json::Value>(idx) {
v
} else {
tracing::warn!(
column = %column_name,
pg_type = %column.type_(),
"row_to_map: column type not representable as JSON; returning null"
);
serde_json::Value::Null
};
map.insert(column_name, value);
}
map
}
pub(super) async fn apply_session_vars(
txn: &tokio_postgres::Transaction<'_>,
session_vars: &[(&str, &str)],
) -> Result<()> {
for (name, value) in session_vars {
if *value == crate::changelog::CLOCK_TIMESTAMP_DIRECTIVE {
txn.execute("SELECT set_config($1, clock_timestamp()::text, true)", &[name])
.await
.map_err(|e| FraiseQLError::Database {
message: format!("set_config({name:?}, clock_timestamp()) failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
} else {
txn.execute("SELECT set_config($1, $2, true)", &[name, value])
.await
.map_err(|e| FraiseQLError::Database {
message: format!("set_config({name:?}) failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
}
}
Ok(())
}
pub(super) async fn mark_cdc_mediated(txn: &tokio_postgres::Transaction<'_>) -> Result<()> {
txn.execute(
"SELECT set_config($1, $2, true)",
&[
&crate::changelog::CDC_MEDIATED_VAR,
&crate::changelog::CDC_MEDIATED_ON,
],
)
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to set {} marker: {e}", crate::changelog::CDC_MEDIATED_VAR),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
Ok(())
}
pub(super) fn build_changelog_cte_sql(quoted_fn: &str, n_args: usize, pre_image: bool) -> String {
let placeholders: Vec<String> = (1..=n_args).map(|i| format!("${i}")).collect();
let object_type_idx = n_args + 1;
let modification_type_idx = n_args + 2;
let tenant_id_idx = n_args + 3;
let trace_id_idx = n_args + 4;
let schema_version_idx = n_args + 5;
let trace_context_idx = n_args + 6;
let actor_type_idx = n_args + 7;
let acting_for_idx = n_args + 8;
let started_var = crate::changelog::STARTED_AT_VAR;
let duration = crate::changelog::duration_ms_sql(started_var);
let calc_version = crate::changelog::DURATION_CALC_VERSION;
let after_before_cols = if pre_image {
"object_data, object_data_before"
} else {
"object_data"
};
let after_before_vals = if pre_image {
"r.entity, r.entity_before"
} else {
"r.entity"
};
format!(
"WITH r AS MATERIALIZED (SELECT * FROM {quoted_fn}({args})), \
_changelog AS ( \
INSERT INTO core.tb_entity_change_log \
(object_type, modification_type, object_id, {after_before_cols}, \
updated_fields, cascade, started_at, duration_ms, extra_metadata, \
tenant_id, trace_id, schema_version, trace_context, \
actor_type, acting_for, commit_time) \
SELECT \
COALESCE(r.entity_type, ${object_type_idx}), \
${modification_type_idx}, \
r.entity_id, {after_before_vals}, r.updated_fields, r.cascade, \
current_setting('{started_var}')::timestamptz, \
{duration}, \
jsonb_build_object('duration_calc_version', {calc_version}::int), \
${tenant_id_idx}::uuid, \
${trace_id_idx}, \
${schema_version_idx}, \
${trace_context_idx}::jsonb, \
${actor_type_idx}, \
${acting_for_idx}::uuid, \
clock_timestamp() \
FROM r \
WHERE r.succeeded AND r.state_changed \
RETURNING 1 \
) \
SELECT * FROM r",
args = placeholders.join(", "),
)
}
async fn prepare_cached_stmt(
client: &deadpool_postgres::Client,
sql: &str,
) -> Result<tokio_postgres::Statement> {
client.prepare_cached(sql).await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to prepare statement: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})
}
#[async_trait]
impl DatabaseAdapter for PostgresAdapter {
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>> {
self.execute_with_projection_impl(view, projection, where_clause, limit, offset, order_by)
.await
}
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>> {
let (sql, typed_params) =
build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
let param_refs = crate::types::as_sql_param_refs(&typed_params);
self.execute_raw(&sql, ¶m_refs)
.await
.map_err(|e| enrich_undefined_column_error(e, view, where_clause))
}
async fn explain_where_query(
&self,
view: &str,
where_clause: Option<&WhereClause>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<serde_json::Value> {
let (select_sql, typed_params) = build_where_select_sql(view, where_clause, limit, offset)?;
if select_sql.contains(';') {
return Err(FraiseQLError::Validation {
message: "EXPLAIN SQL must be a single statement".into(),
path: None,
});
}
let explain_sql = format!("EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) {select_sql}");
let param_refs = crate::types::as_sql_param_refs(&typed_params);
let client = self.acquire_connection_with_retry().await?;
let rows = client.query(explain_sql.as_str(), ¶m_refs).await.map_err(|e| {
FraiseQLError::Database {
message: format!("EXPLAIN ANALYZE failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
if let Some(row) = rows.first() {
let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
message: format!("Failed to parse EXPLAIN output: {e}"),
sql_state: None,
})?;
Ok(plan)
} else {
Ok(serde_json::Value::Null)
}
}
fn database_type(&self) -> DatabaseType {
DatabaseType::PostgreSQL
}
async fn health_check(&self) -> Result<()> {
let client = self.acquire_connection_with_retry().await?;
client.query("SELECT 1", &[]).await.map_err(|e| FraiseQLError::Database {
message: format!("Health check failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
Ok(())
}
#[allow(clippy::cast_possible_truncation)] fn pool_metrics(&self) -> PoolMetrics {
let status = self.pool.status();
PoolMetrics {
total_connections: status.size as u32,
idle_connections: status.available as u32,
active_connections: (status.size - status.available) as u32,
waiting_requests: status.waiting as u32,
}
}
async fn execute_raw_query(
&self,
sql: &str,
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let client = self.acquire_connection_with_retry().await?;
let rows: Vec<Row> = client.query(sql, &[]).await.map_err(|e| FraiseQLError::Database {
message: format!("Query execution failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
rows.iter().map(row_to_map).collect();
Ok(results)
}
async fn execute_parameterized_aggregate(
&self,
sql: &str,
params: &[serde_json::Value],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
let param_refs = crate::types::as_sql_param_refs(&typed);
let client = self.acquire_connection_with_retry().await?;
let rows: Vec<Row> =
client.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
message: format!("Parameterized aggregate query failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
rows.iter().map(row_to_map).collect();
Ok(results)
}
async fn execute_parameterized_aggregate_with_session(
&self,
sql: &str,
params: &[serde_json::Value],
session_vars: &[(&str, &str)],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
if session_vars.is_empty() {
return self.execute_parameterized_aggregate(sql, params).await;
}
let typed: Vec<QueryParam> = params.iter().cloned().map(QueryParam::from).collect();
let param_refs = crate::types::as_sql_param_refs(&typed);
let mut client = self.acquire_connection_with_retry().await?;
let txn =
client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start session-var transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
apply_session_vars(&txn, session_vars).await?;
let rows: Vec<Row> =
txn.query(sql, ¶m_refs).await.map_err(|e| FraiseQLError::Database {
message: format!("Parameterized aggregate query failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit session-var transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
Ok(rows.iter().map(row_to_map).collect())
}
async fn execute_function_call(
&self,
function_name: &str,
args: &[serde_json::Value],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let quoted_fn = quote_postgres_identifier(function_name);
let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
let mut client = self.acquire_connection_with_retry().await?;
let flex_args: Vec<FlexParam> = args
.iter()
.map(|v| match v {
serde_json::Value::Null => FlexParam::Null,
serde_json::Value::String(s) => FlexParam::Text(s.clone()),
_ => FlexParam::Text(v.to_string()),
})
.collect();
let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
.iter()
.map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
.collect();
let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
if self.mutation_timing_enabled {
let txn =
client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start mutation timing transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
txn.execute(
"SELECT set_config($1, clock_timestamp()::text, true)",
&[&self.timing_variable_name],
)
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to set mutation timing variable: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
let detail = e.as_db_error().map_or("", |d| d.message());
FraiseQLError::Database {
message: format!("Function call {function_name} failed: {e}: {detail}"),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit mutation timing transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
rows.iter().map(row_to_map).collect();
Ok(results)
} else {
let rows: Vec<Row> = client.query(&stmt, params.as_slice()).await.map_err(|e| {
let detail = e.as_db_error().map_or("", |d| d.message());
FraiseQLError::Database {
message: format!("Function call {function_name} failed: {e}: {detail}"),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
let results: Vec<std::collections::HashMap<String, serde_json::Value>> =
rows.iter().map(row_to_map).collect();
Ok(results)
}
}
async fn execute_function_call_with_session(
&self,
function_name: &str,
args: &[serde_json::Value],
session_vars: &[(&str, &str)],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
if session_vars.is_empty() && !self.mutation_timing_enabled {
return self.execute_function_call(function_name, args).await;
}
let quoted_fn = quote_postgres_identifier(function_name);
let placeholders: Vec<String> = (1..=args.len()).map(|i| format!("${i}")).collect();
let sql = format!("SELECT * FROM {quoted_fn}({})", placeholders.join(", "));
let flex_args: Vec<FlexParam> = args
.iter()
.map(|v| match v {
serde_json::Value::Null => FlexParam::Null,
serde_json::Value::String(s) => FlexParam::Text(s.clone()),
_ => FlexParam::Text(v.to_string()),
})
.collect();
let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
.iter()
.map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
.collect();
let mut client = self.acquire_connection_with_retry().await?;
let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
let txn =
client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start session-var transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
apply_session_vars(&txn, session_vars).await?;
mark_cdc_mediated(&txn).await?;
if self.mutation_timing_enabled {
txn.execute(
"SELECT set_config($1, clock_timestamp()::text, true)",
&[&self.timing_variable_name],
)
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to set mutation timing variable: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
}
let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
let detail = e.as_db_error().map_or("", |d| d.message());
FraiseQLError::Database {
message: format!("Function call {function_name} failed: {e}: {detail}"),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit session-var transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
Ok(rows.iter().map(row_to_map).collect())
}
async fn execute_function_call_with_changelog(
&self,
function_name: &str,
args: &[serde_json::Value],
session_vars: &[(&str, &str)],
changelog: Option<&crate::traits::ChangeLogWrite<'_>>,
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
let Some(changelog) = changelog else {
return self
.execute_function_call_with_session(function_name, args, session_vars)
.await;
};
let quoted_fn = quote_postgres_identifier(function_name);
let sql = build_changelog_cte_sql("ed_fn, args.len(), changelog.pre_image);
let mut flex_args: Vec<FlexParam> = args
.iter()
.map(|v| match v {
serde_json::Value::Null => FlexParam::Null,
serde_json::Value::String(s) => FlexParam::Text(s.clone()),
_ => FlexParam::Text(v.to_string()),
})
.collect();
flex_args.push(FlexParam::Text(changelog.object_type.to_string()));
flex_args.push(FlexParam::Text(changelog.modification_type.to_string()));
flex_args
.push(changelog.tenant_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
flex_args
.push(changelog.trace_id.map_or(FlexParam::Null, |t| FlexParam::Text(t.to_string())));
flex_args.push(
changelog
.schema_version
.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
);
flex_args.push(
changelog
.trace_context
.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())),
);
flex_args
.push(changelog.actor_type.map_or(FlexParam::Null, |s| FlexParam::Text(s.to_string())));
flex_args
.push(changelog.acting_for.map_or(FlexParam::Null, |u| FlexParam::Text(u.to_string())));
let params: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = flex_args
.iter()
.map(|v| v as &(dyn tokio_postgres::types::ToSql + Sync))
.collect();
let mut client = self.acquire_connection_with_retry().await?;
let stmt = prepare_cached_stmt(&client, sql.as_str()).await?;
let txn =
client.build_transaction().start().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to start change-log outbox transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
apply_session_vars(&txn, session_vars).await?;
mark_cdc_mediated(&txn).await?;
if self.mutation_timing_enabled {
txn.execute(
"SELECT set_config($1, clock_timestamp()::text, true)",
&[&self.timing_variable_name],
)
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to set mutation timing variable: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
}
let started_at_set =
session_vars.iter().any(|(name, _)| *name == crate::changelog::STARTED_AT_VAR)
|| (self.mutation_timing_enabled
&& self.timing_variable_name == crate::changelog::STARTED_AT_VAR);
if !started_at_set {
txn.execute(
"SELECT set_config($1, clock_timestamp()::text, true)",
&[&crate::changelog::STARTED_AT_VAR],
)
.await
.map_err(|e| FraiseQLError::Database {
message: format!("Failed to stamp change-log started_at: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
}
let rows: Vec<Row> = txn.query(&stmt, params.as_slice()).await.map_err(|e| {
let detail = e.as_db_error().map_or("", |d| d.message());
FraiseQLError::Database {
message: format!(
"Function call {function_name} (with change-log outbox) failed: {e}: {detail}"
),
sql_state: e.code().map(|c| c.code().to_string()),
}
})?;
txn.commit().await.map_err(|e| FraiseQLError::Database {
message: format!("Failed to commit change-log outbox transaction: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
Ok(rows.iter().map(row_to_map).collect())
}
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)],
) -> Result<Arc<Vec<JsonbValue>>> {
if session_vars.is_empty() {
return self.execute_where_query_arc(view, where_clause, limit, offset, order_by).await;
}
let (sql, typed_params) =
build_where_select_sql_ordered(view, where_clause, limit, offset, order_by)?;
let param_refs = crate::types::as_sql_param_refs(&typed_params);
self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
.await
.map(Arc::new)
.map_err(|e| enrich_undefined_column_error(e, view, where_clause))
}
async fn execute_with_projection_arc_with_session(
&self,
request: &ProjectionRequest<'_>,
session_vars: &[(&str, &str)],
) -> Result<Arc<Vec<JsonbValue>>> {
if session_vars.is_empty() {
return self.execute_with_projection_arc(request).await;
}
let Some(projection) = request.projection else {
return self
.execute_where_query_arc_with_session(
request.view,
request.where_clause,
request.limit,
request.offset,
request.order_by,
session_vars,
)
.await;
};
let (sql, typed_params) = build_projection_select_sql(
projection,
request.view,
request.where_clause,
request.limit,
request.offset,
request.order_by,
)?;
let param_refs = crate::types::as_sql_param_refs(&typed_params);
self.execute_raw_with_session(&sql, ¶m_refs, session_vars)
.await
.map(Arc::new)
}
async fn explain_query(
&self,
sql: &str,
_params: &[serde_json::Value],
) -> Result<serde_json::Value> {
if sql.contains(';') {
return Err(FraiseQLError::Validation {
message: "EXPLAIN SQL must be a single statement".into(),
path: None,
});
}
let explain_sql = format!("EXPLAIN (ANALYZE false, FORMAT JSON) {sql}");
let client = self.acquire_connection_with_retry().await?;
let rows: Vec<Row> =
client
.query(explain_sql.as_str(), &[])
.await
.map_err(|e| FraiseQLError::Database {
message: format!("EXPLAIN failed: {e}"),
sql_state: e.code().map(|c| c.code().to_string()),
})?;
if let Some(row) = rows.first() {
let plan: serde_json::Value = row.try_get(0).map_err(|e| FraiseQLError::Database {
message: format!("Failed to parse EXPLAIN output: {e}"),
sql_state: None,
})?;
Ok(plan)
} else {
Ok(serde_json::Value::Null)
}
}
async fn query_stats(&self, limit: u32) -> Result<Vec<crate::types::QueryStatEntry>> {
self.pg_query_stats(limit).await
}
async fn query_stats_by_id(&self, id: &str) -> Result<Option<crate::types::QueryStatEntry>> {
self.pg_query_stats_by_id(id).await
}
async fn reset_query_stats(&self) -> Result<()> {
self.pg_reset_query_stats().await
}
}
impl SupportsMutations for PostgresAdapter {}