use crate::DjogiError;
use crate::context::ContextInner;
use crate::model::Model;
use crate::pg::accumulator::{SqlAccumulator, as_params};
use crate::pg::decode::FromPgRow;
use crate::relation::path::RelationPath;
use postgres_types::ToSql;
use std::any::Any;
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use tokio_postgres::types::FromSql;
pub(crate) type ResolvedTargetSlot = Option<Box<dyn Any + Send + Sync>>;
pub(crate) type AlignedTargets = Vec<ResolvedTargetSlot>;
pub struct PrefetchedRow<T: Model> {
pub row: T,
relations: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
}
impl<T: Model + std::fmt::Debug> std::fmt::Debug for PrefetchedRow<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let keys: Vec<&&'static str> = self.relations.keys().collect();
f.debug_struct("PrefetchedRow")
.field("row", &self.row)
.field("resolved_relations", &keys)
.finish()
}
}
impl<T: Model> PrefetchedRow<T> {
pub fn get<Target: Model + 'static>(&self, path: RelationPath<T, Target>) -> Option<&Target> {
self.relations
.get(path.source_column())
.and_then(|b| b.downcast_ref::<Target>())
}
pub(crate) fn new(
row: T,
relations: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
) -> Self {
Self { row, relations }
}
}
pub(crate) type PrefetchLoaderFn = for<'a> fn(
exec: &'a mut ContextInner,
parent_table: &'static str,
source_column: &'static str,
parent_pks: Vec<Box<dyn Any + Send + Sync>>,
) -> Pin<
Box<dyn Future<Output = Result<AlignedTargets, DjogiError>> + Send + 'a>,
>;
#[derive(Clone)]
pub(crate) struct ErasedPrefetch {
pub source_column: &'static str,
pub parent_table: &'static str,
pub loader: PrefetchLoaderFn,
}
impl std::fmt::Debug for ErasedPrefetch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ErasedPrefetch")
.field("source_column", &self.source_column)
.field("parent_table", &self.parent_table)
.finish_non_exhaustive()
}
}
pub(crate) fn prefetch_loader<'a, Source, Target>(
exec: &'a mut ContextInner,
parent_table: &'static str,
source_column: &'static str,
parent_pks: Vec<Box<dyn Any + Send + Sync>>,
) -> Pin<Box<dyn Future<Output = Result<AlignedTargets, DjogiError>> + Send + 'a>>
where
Source: Model,
Source::Pk: ToSql + for<'r> FromSql<'r> + Eq + Hash + Clone + Send + Sync + 'static,
Target: Model + FromPgRow + Clone + Send + Unpin + 'static,
{
Box::pin(async move {
let parent_pks_typed: Vec<Source::Pk> = parent_pks
.into_iter()
.map(|b| {
*b.downcast::<Source::Pk>().expect(
"prefetch_loader: parent PK downcast — type mismatch is a framework bug",
)
})
.collect();
let mut seen: HashMap<Source::Pk, ()> = HashMap::with_capacity(parent_pks_typed.len());
let mut unique_pks: Vec<Source::Pk> = Vec::with_capacity(parent_pks_typed.len());
for pk in &parent_pks_typed {
if seen.insert(pk.clone(), ()).is_none() {
unique_pks.push(pk.clone());
}
}
if unique_pks.is_empty() {
let mut out: Vec<Option<Box<dyn Any + Send + Sync>>> =
Vec::with_capacity(parent_pks_typed.len());
out.resize_with(parent_pks_typed.len(), || None);
return Ok(out);
}
let target_table = <Target as Model>::table_name();
let target_fields = <Target as Model>::descriptor().fields;
let mut acc = SqlAccumulator::new("SELECT ");
for (i, field) in target_fields.iter().enumerate() {
crate::ident::debug_assert_ident!(field.name, "field_name");
if i > 0 {
acc.push_sql(", ");
}
acc.push_sql("t.");
acc.push_sql(field.name);
}
acc.push_sql(", p.id AS __djogi_parent_id");
acc.push_sql(" FROM ");
acc.push_sql(parent_table);
acc.push_sql(" p LEFT JOIN ");
acc.push_sql(target_table);
acc.push_sql(" t ON t.id = p.");
acc.push_sql(source_column);
acc.push_sql(" WHERE p.id IN (");
acc.push_list_binds(unique_pks.iter().cloned());
acc.push_sql(")");
let (sql, binds) = acc.into_parts();
let params = as_params(&binds);
let rows = match exec {
ContextInner::Pool(pool) => {
let mut conn = pool.get().await?;
conn.query(&sql, ¶ms).await?
}
ContextInner::Transaction(conn) => conn.query(&sql, ¶ms).await?,
};
let mut parent_to_target: HashMap<Source::Pk, Option<Target>> =
HashMap::with_capacity(rows.len());
for row in rows {
let parent_pk: Source::Pk =
row.try_get("__djogi_parent_id").map_err(DjogiError::from)?;
let target_pk_opt: Option<Target::Pk> = row.try_get("id").map_err(DjogiError::from)?;
let target_is_null = target_pk_opt.is_none();
let slot = if target_is_null {
None
} else {
Some(Target::from_pg_row(&row)?)
};
parent_to_target.entry(parent_pk).or_insert(slot);
}
let mut out: Vec<Option<Box<dyn Any + Send + Sync>>> =
Vec::with_capacity(parent_pks_typed.len());
for pk in &parent_pks_typed {
match parent_to_target.get(pk) {
Some(Some(t)) => {
out.push(Some(Box::new(t.clone()) as Box<dyn Any + Send + Sync>));
}
Some(None) | None => {
out.push(None);
}
}
}
Ok(out)
})
}
pub(crate) fn extract_parent_pks<T: Model>(rows: &[T]) -> Vec<Box<dyn Any + Send + Sync>>
where
T::Pk: Clone + Send + Sync + 'static,
{
rows.iter()
.map(|r| Box::new(r.pk_value().clone()) as Box<dyn Any + Send + Sync>)
.collect()
}
pub(crate) async fn apply_prefetches<T>(
exec: &mut ContextInner,
prefetches: &[ErasedPrefetch],
rows: Vec<T>,
) -> Result<Vec<PrefetchedRow<T>>, DjogiError>
where
T: Model,
T::Pk: Clone + Send + Sync + 'static,
{
if rows.is_empty() {
return Ok(Vec::new());
}
let parent_pks = extract_parent_pks(&rows);
let mut per_path_results: Vec<(&'static str, AlignedTargets)> =
Vec::with_capacity(prefetches.len());
for prefetch in prefetches {
let parent_pks_for_loader: Vec<Box<dyn Any + Send + Sync>> = parent_pks
.iter()
.map(|pk_box| {
let pk: &T::Pk = pk_box
.downcast_ref::<T::Pk>()
.expect("extract_parent_pks box is T::Pk — framework invariant");
Box::new(pk.clone()) as Box<dyn Any + Send + Sync>
})
.collect();
let aligned = (prefetch.loader)(
&mut *exec,
prefetch.parent_table,
prefetch.source_column,
parent_pks_for_loader,
)
.await?;
per_path_results.push((prefetch.source_column, aligned));
}
let mut out: Vec<PrefetchedRow<T>> = Vec::with_capacity(rows.len());
for (i, row) in rows.into_iter().enumerate() {
let mut relations: HashMap<&'static str, Box<dyn Any + Send + Sync>> =
HashMap::with_capacity(per_path_results.len());
for (source_column, aligned) in per_path_results.iter_mut() {
if let Some(slot) = aligned.get_mut(i)
&& let Some(boxed_target) = slot.take()
{
relations.insert(*source_column, boxed_target);
}
}
out.push(PrefetchedRow::new(row, relations));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ModelDescriptor;
use crate::relation::path::{RelationKind, RelationPath};
use crate::types::HeerId;
use std::future::Future;
#[derive(Debug, Clone)]
struct Parent;
#[derive(Debug, Clone)]
struct Child;
macro_rules! dummy_model {
($ty:ty, $table:literal) => {
impl crate::model::__sealed::Sealed for $ty {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for $ty {
type Pk = HeerId;
type Fields = ();
fn table_name() -> &'static str {
$table
}
fn pk_value(&self) -> &HeerId {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: HeerId,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx {
async { unreachable!() }
}
}
};
}
dummy_model!(Parent, "parents");
dummy_model!(Child, "children");
#[test]
fn prefetched_row_get_returns_target_when_present() {
let row = Parent;
let child = Child;
let mut relations: HashMap<&'static str, Box<dyn Any + Send + Sync>> = HashMap::new();
relations.insert("child_id", Box::new(child) as Box<dyn Any + Send + Sync>);
let pr: PrefetchedRow<Parent> = PrefetchedRow::new(row, relations);
let path: RelationPath<Parent, Child> =
RelationPath::new("child_id", "children", RelationKind::ForeignKey);
assert!(pr.get(path).is_some(), "resolved child should be present");
}
#[test]
fn prefetched_row_get_returns_none_when_missing() {
let pr: PrefetchedRow<Parent> = PrefetchedRow::new(Parent, HashMap::new());
let path: RelationPath<Parent, Child> =
RelationPath::new("child_id", "children", RelationKind::ForeignKey);
assert!(pr.get(path).is_none());
}
#[test]
fn prefetched_row_debug_lists_relation_keys() {
let mut relations: HashMap<&'static str, Box<dyn Any + Send + Sync>> = HashMap::new();
relations.insert("child_id", Box::new(Child) as Box<dyn Any + Send + Sync>);
let pr: PrefetchedRow<Parent> = PrefetchedRow::new(Parent, relations);
let debug = format!("{pr:?}");
assert!(
debug.contains("child_id"),
"Debug should list the resolved relation keys, got: {debug}"
);
}
#[test]
fn erased_prefetch_debug_omits_loader_fn_pointer() {
fn stub_loader<'a>(
_exec: &'a mut ContextInner,
_parent_table: &'static str,
_source_column: &'static str,
_parent_pks: Vec<Box<dyn Any + Send + Sync>>,
) -> Pin<Box<dyn Future<Output = Result<AlignedTargets, DjogiError>> + Send + 'a>> {
Box::pin(async { Ok(Vec::new()) })
}
let e = ErasedPrefetch {
source_column: "child_id",
parent_table: "parents",
loader: stub_loader,
};
let debug = format!("{e:?}");
assert!(debug.contains("child_id"));
assert!(debug.contains("parents"));
assert!(
!debug.contains("0x"),
"fn pointer should not leak into Debug, got: {debug}"
);
}
}