use std::collections::HashMap;
use sea_query::{PostgresQueryBuilder, SqliteQueryBuilder};
use sea_query_binder::SqlxBinder;
use serde_json::Value as JsonValue;
use crate::db::DbPool;
use crate::orm::{HydrateRelated, Model};
use super::{backend_pg, backend_sqlite};
pub(super) async fn hydrate_select_related<T: Model + HydrateRelated>(
rows: &mut [T],
sr_fields: &[String],
pool: &DbPool,
) -> Result<(), sqlx::Error> {
for field_name in sr_fields {
if field_name.contains("__") {
hydrate_select_related_nested::<T>(rows, field_name, pool).await?;
continue;
}
let field_spec = T::FIELDS
.iter()
.find(|f| f.name == field_name.as_str())
.ok_or_else(|| {
sqlx::Error::Protocol(format!(
"umbral::orm::select_related: unknown field `{field_name}` on model `{}`",
T::NAME
))
})?;
let fk_target = field_spec.fk_target.ok_or_else(|| {
sqlx::Error::Protocol(format!(
"umbral::orm::select_related: field `{field_name}` on `{}` is not a foreign key",
T::NAME
))
})?;
let mut ids: Vec<JsonValue> = Vec::with_capacity(rows.len());
for row in rows.iter() {
if let Some(v) = row.fk_id_for(field_name.as_str()) {
if !v.is_null() {
ids.push(v);
}
}
}
if ids.is_empty() {
continue;
}
dedup_by_pk_key(&mut ids);
let target_meta = if crate::migrate::is_initialised() {
crate::migrate::registered_models()
.into_iter()
.find(|m| m.table == fk_target)
} else {
None
};
let target_soft_delete = target_meta.as_ref().is_some_and(|m| m.soft_delete);
let (target_pk_col, target_pk_ty) = target_meta
.as_ref()
.and_then(|m| m.pk_column().map(|c| (c.name.clone(), c.ty)))
.or_else(|| crate::migrate::pk_meta_for_table(fk_target))
.unwrap_or_else(|| ("id".to_string(), crate::orm::SqlType::BigInt));
let related_rows = fetch_related_as_json_by_pk(
fk_target,
&target_pk_col,
target_pk_ty,
target_soft_delete,
&ids,
pool,
)
.await?;
let id_to_json: HashMap<String, JsonValue> = related_rows
.into_iter()
.filter_map(|obj| {
let map = obj.as_object()?;
let pk_val = map.get(target_pk_col.as_str())?;
Some((pk_json_key(pk_val), obj.clone()))
})
.collect();
for row in rows.iter_mut() {
if let Some(fk_val) = row.fk_id_for(field_name.as_str()) {
if let Some(resolved_json) = id_to_json.get(&pk_json_key(&fk_val)) {
row.hydrate_fk(field_name.as_str(), resolved_json);
}
}
}
}
Ok(())
}
fn pk_json_key(v: &JsonValue) -> String {
crate::orm::pk_key(v)
}
fn dedup_by_pk_key(ids: &mut Vec<JsonValue>) {
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
ids.retain(|v| seen.insert(pk_json_key(v)));
}
fn parent_pk_sql_type<T: Model>() -> crate::orm::SqlType {
crate::migrate::ModelMeta::for_::<T>()
.fields
.iter()
.find(|c| c.primary_key)
.map(|c| c.ty)
.unwrap_or(crate::orm::SqlType::BigInt)
}
pub(super) async fn hydrate_select_related_nested<T: Model + HydrateRelated>(
rows: &mut [T],
path: &str,
pool: &DbPool,
) -> Result<(), sqlx::Error> {
let hops: Vec<&str> = path.split("__").filter(|s| !s.is_empty()).collect();
if hops.is_empty() {
return Ok(());
}
let registered = crate::migrate::registered_models();
let mut current_table = T::TABLE;
let mut hop_targets: Vec<&str> = Vec::with_capacity(hops.len());
for hop in &hops {
let meta = registered
.iter()
.find(|m| m.table == current_table)
.ok_or_else(|| {
sqlx::Error::Protocol(format!(
"umbral::orm::select_related: model for table `{current_table}` is not registered \
(needed for nested traversal of `{path}`)"
))
})?;
let col = meta.fields.iter().find(|c| c.name == *hop).ok_or_else(|| {
sqlx::Error::Protocol(format!(
"umbral::orm::select_related: unknown field `{hop}` on table `{current_table}` \
(full path `{path}`)"
))
})?;
let target = col.fk_target.as_deref().ok_or_else(|| {
sqlx::Error::Protocol(format!(
"umbral::orm::select_related: field `{hop}` on table `{current_table}` is not a \
foreign key (full path `{path}`)"
))
})?;
hop_targets.push(target);
current_table = target;
}
let hop_target_pk: Vec<(String, crate::orm::SqlType)> = hop_targets
.iter()
.filter_map(|t| {
registered
.iter()
.find(|m| &m.table == t)
.and_then(|m| m.pk_column().map(|c| (c.name.clone(), c.ty)))
})
.collect();
if hop_target_pk.len() != hops.len() {
return Ok(());
}
let hop_target_soft_delete: Vec<bool> = hop_targets
.iter()
.map(|t| {
registered
.iter()
.find(|m| &m.table == t)
.is_some_and(|m| m.soft_delete)
})
.collect();
let first_field = hops[0];
let mut ids: Vec<JsonValue> = rows
.iter()
.filter_map(|r| {
let v = r.fk_id_for(first_field)?;
if v.is_null() { None } else { Some(v) }
})
.collect();
if ids.is_empty() {
return Ok(());
}
dedup_by_pk_key(&mut ids);
let mut levels: Vec<Vec<JsonValue>> = Vec::with_capacity(hops.len());
levels.push(
fetch_related_as_json_by_pk(
hop_targets[0],
&hop_target_pk[0].0,
hop_target_pk[0].1,
hop_target_soft_delete[0],
&ids,
pool,
)
.await?,
);
for hop_idx in 1..hops.len() {
let hop_field = hops[hop_idx];
let hop_target = hop_targets[hop_idx];
let prev_lvl = &levels[hop_idx - 1];
let mut next_ids: Vec<JsonValue> = prev_lvl
.iter()
.filter_map(|r| {
let v = r.as_object()?.get(hop_field)?;
if v.is_null() { None } else { Some(v.clone()) }
})
.collect();
if next_ids.is_empty() {
break;
}
dedup_by_pk_key(&mut next_ids);
levels.push(
fetch_related_as_json_by_pk(
hop_target,
&hop_target_pk[hop_idx].0,
hop_target_pk[hop_idx].1,
hop_target_soft_delete[hop_idx],
&next_ids,
pool,
)
.await?,
);
}
if levels.len() > 1 {
for i in (0..levels.len() - 1).rev() {
let next_pk_col = &hop_target_pk[i + 1].0;
let next_by_pk: HashMap<String, JsonValue> = levels[i + 1]
.iter()
.filter_map(|obj| {
let map = obj.as_object()?;
let pk_val = map.get(next_pk_col.as_str())?;
Some((pk_json_key(pk_val), obj.clone()))
})
.collect();
let hop_field = hops[i + 1];
for row in levels[i].iter_mut() {
let Some(map) = row.as_object_mut() else {
continue;
};
let Some(fk_val) = map.get(hop_field) else {
continue;
};
if fk_val.is_null() {
continue;
}
if let Some(next_json) = next_by_pk.get(&pk_json_key(fk_val)) {
map.insert(hop_field.to_string(), next_json.clone());
}
}
}
}
let first_pk_col = &hop_target_pk[0].0;
let first_by_pk: HashMap<String, JsonValue> = levels
.into_iter()
.next()
.unwrap_or_default()
.into_iter()
.filter_map(|obj| {
let map = obj.as_object()?;
let pk_val = map.get(first_pk_col.as_str())?;
Some((pk_json_key(pk_val), obj.clone()))
})
.collect();
for row in rows.iter_mut() {
if let Some(fk_val) = row.fk_id_for(first_field) {
if let Some(json) = first_by_pk.get(&pk_json_key(&fk_val)) {
row.hydrate_fk(first_field, json);
}
}
}
Ok(())
}
pub(super) async fn hydrate_reverse_fk_for_field<T: Model + HydrateRelated>(
rows: &mut [T],
spec: &crate::orm::model::ReverseFkRelationSpec,
pool: &DbPool,
) -> Result<(), sqlx::Error> {
let mut parent_pks: Vec<JsonValue> = rows.iter().filter_map(|r| r.pk_as_json()).collect();
if parent_pks.is_empty() {
for r in rows.iter_mut() {
r.set_reverse_fk_resolved_json(spec.field_name, Vec::new());
}
return Ok(());
}
dedup_by_pk_key(&mut parent_pks);
let parent_pk_ty = parent_pk_sql_type::<T>();
let child_soft_delete = crate::migrate::is_initialised()
&& crate::migrate::registered_models()
.into_iter()
.find(|m| m.table == spec.target_table)
.is_some_and(|m| m.soft_delete);
let child_rows = fetch_related_as_json_by_pk(
spec.target_table,
spec.fk_column,
parent_pk_ty,
child_soft_delete,
&parent_pks,
pool,
)
.await?;
let mut by_parent: HashMap<String, Vec<JsonValue>> = HashMap::new();
for row in child_rows {
let key = row
.as_object()
.and_then(|m| m.get(spec.fk_column))
.map(crate::orm::pk_key);
if let Some(key) = key {
by_parent.entry(key).or_default().push(row);
}
}
for row in rows.iter_mut() {
if let Some(pk) = row.pk_as_json() {
let bucket = by_parent
.remove(&crate::orm::pk_key(&pk))
.unwrap_or_default();
row.set_reverse_fk_resolved_json(spec.field_name, bucket);
}
}
Ok(())
}
async fn hydrate_one_to_one_for_field<T: Model + HydrateRelated>(
rows: &mut [T],
spec: &crate::orm::model::OneToOneRelationSpec,
pool: &DbPool,
) -> Result<(), sqlx::Error> {
let registered = crate::migrate::registered_models();
let Some(child_meta) = registered.iter().find(|m| m.table == spec.target_table) else {
return Err(sqlx::Error::Protocol(format!(
"umbral::orm::prefetch_related: child model for table `{}` is not \
registered (needed by OneToOne field `{}` on `{}`)",
spec.target_table,
spec.field_name,
T::NAME,
)));
};
let candidates: Vec<&str> = child_meta
.fields
.iter()
.filter(|c| c.unique && c.fk_target.as_deref() == Some(T::TABLE))
.map(|c| c.name.as_str())
.collect();
let fk_column = match candidates.len() {
1 => candidates[0],
0 => {
return Err(sqlx::Error::Protocol(format!(
"umbral::orm::prefetch_related: OneToOne field `{}` on `{}` \
has no back-link — `{}` needs a `#[umbral(unique)]` \
ForeignKey<{}> pointing back (none found)",
spec.field_name,
T::NAME,
spec.target_name,
T::NAME
)));
}
_ => {
return Err(sqlx::Error::Protocol(format!(
"umbral::orm::prefetch_related: OneToOne field `{}` on `{}` \
is ambiguous — `{}` has multiple UNIQUE ForeignKey<{}> \
columns ({}). Rename one or use a typed ReverseSet field \
instead.",
spec.field_name,
T::NAME,
spec.target_name,
T::NAME,
candidates.join(", "),
)));
}
};
let mut parent_pks: Vec<JsonValue> = rows.iter().filter_map(|r| r.pk_as_json()).collect();
if parent_pks.is_empty() {
for r in rows.iter_mut() {
r.set_one_to_one_resolved_json(spec.field_name, None);
}
return Ok(());
}
dedup_by_pk_key(&mut parent_pks);
let parent_pk_ty = parent_pk_sql_type::<T>();
let child_rows = fetch_related_as_json_by_pk(
spec.target_table,
fk_column,
parent_pk_ty,
child_meta.soft_delete,
&parent_pks,
pool,
)
.await?;
let mut by_parent: HashMap<String, JsonValue> = HashMap::new();
for row in child_rows {
let key = row
.as_object()
.and_then(|m| m.get(fk_column))
.map(crate::orm::pk_key);
if let Some(key) = key {
by_parent.entry(key).or_insert(row);
}
}
for row in rows.iter_mut() {
if let Some(pk) = row.pk_as_json() {
let child = by_parent.remove(&crate::orm::pk_key(&pk));
row.set_one_to_one_resolved_json(spec.field_name, child);
}
}
Ok(())
}
pub(super) async fn hydrate_prefetch_related<T: Model + HydrateRelated>(
rows: &mut [T],
prefetch_fields: &[String],
pool: &DbPool,
) -> Result<(), sqlx::Error> {
for field_name in prefetch_fields {
let m2m_spec = T::M2M_RELATIONS
.iter()
.find(|s| s.field_name == field_name.as_str());
let rfk_spec = T::REVERSE_FK_RELATIONS
.iter()
.find(|s| s.field_name == field_name.as_str());
if let Some(spec) = rfk_spec {
hydrate_reverse_fk_for_field::<T>(rows, spec, pool).await?;
continue;
}
let o2o_spec = T::ONE_TO_ONE_RELATIONS
.iter()
.find(|s| s.field_name == field_name.as_str());
if let Some(spec) = o2o_spec {
hydrate_one_to_one_for_field::<T>(rows, spec, pool).await?;
continue;
}
let spec = match m2m_spec {
Some(s) => s,
None => {
let is_fk = T::FIELDS
.iter()
.any(|f| f.name == field_name.as_str() && f.fk_target.is_some());
let hint = if is_fk {
format!(
" — `{field_name}` is a foreign key, use `.select_related(...)` \
or `.join_related(...)` instead"
)
} else {
" — no M2M, ReverseSet, or OneToOne field with that name on this model"
.to_string()
};
return Err(sqlx::Error::Protocol(format!(
"umbral::orm::prefetch_related: unknown field `{field_name}` on model `{}`{hint}",
T::NAME
)));
}
};
let junction_table = format!("{}_{}", T::TABLE, spec.field_name);
let registered: Vec<crate::migrate::ModelMeta> = crate::migrate::registered_models();
let child_meta = match registered
.into_iter()
.find(|m| m.table == spec.target_table)
{
Some(m) => m,
None => continue,
};
let child_pk_col = match child_meta.fields.iter().find(|c| c.primary_key) {
Some(c) => c.name.clone(),
None => continue,
};
let mut parent_pks: Vec<JsonValue> = rows.iter().filter_map(|r| r.pk_as_json()).collect();
if parent_pks.is_empty() {
for r in rows.iter_mut() {
r.set_m2m_resolved_json(field_name.as_str(), Vec::new());
}
continue;
}
dedup_by_pk_key(&mut parent_pks);
let parent_id_col = {
let meta = crate::migrate::ModelMeta::for_::<T>();
match meta.fields.into_iter().find(|c| c.primary_key) {
Some(mut c) => {
c.name = "__parent_id".to_string();
c
}
None => continue, }
};
let parent_seavals: Vec<sea_query::SimpleExpr> = parent_pks
.iter()
.filter_map(|v| {
crate::orm::write::json_to_sea_value(
parent_id_col.ty,
v,
false,
"__parent_id",
None,
)
.ok()
.map(sea_query::SimpleExpr::Value)
})
.collect();
let mut q = sea_query::Query::select();
q.expr_as(
sea_query::Expr::col((
sea_query::Alias::new("j"),
sea_query::Alias::new("parent_id"),
)),
sea_query::Alias::new("__parent_id"),
);
for col in &child_meta.fields {
q.expr_as(
sea_query::Expr::col((
sea_query::Alias::new("c"),
sea_query::Alias::new(col.name.as_str()),
)),
sea_query::Alias::new(col.name.as_str()),
);
}
q.from_as(
crate::db::router::schema_qualified_table(child_meta.table.as_str()),
sea_query::Alias::new("c"),
)
.join_as(
sea_query::JoinType::InnerJoin,
crate::db::router::schema_qualified_table(&junction_table),
sea_query::Alias::new("j"),
sea_query::Expr::col((
sea_query::Alias::new("j"),
sea_query::Alias::new("child_id"),
))
.equals((
sea_query::Alias::new("c"),
sea_query::Alias::new(child_pk_col.as_str()),
)),
)
.and_where(
sea_query::Expr::col((
sea_query::Alias::new("j"),
sea_query::Alias::new("parent_id"),
))
.is_in(parent_seavals),
);
if child_meta.soft_delete {
q.and_where(
sea_query::Expr::col((
sea_query::Alias::new("c"),
sea_query::Alias::new("deleted_at"),
))
.is_null(),
);
}
let mut buckets: HashMap<String, Vec<JsonValue>> = HashMap::new();
match pool {
DbPool::Sqlite(p) => {
let (sql, vals) = q.build_sqlx(SqliteQueryBuilder);
let raw_rows = sqlx::query_with::<sqlx::Sqlite, _>(&sql, vals)
.fetch_all(p)
.await?;
for raw in &raw_rows {
let parent_json = crate::orm::dynamic::decode_to_json(raw, &parent_id_col)?;
let mut obj = serde_json::Map::with_capacity(child_meta.fields.len());
for col in &child_meta.fields {
let v = crate::orm::dynamic::decode_to_json(raw, col)?;
obj.insert(col.name.clone(), v);
}
buckets
.entry(crate::orm::pk_key(&parent_json))
.or_default()
.push(JsonValue::Object(obj));
}
}
DbPool::Postgres(p) => {
let (sql, vals) = q.build_sqlx(PostgresQueryBuilder);
let raw_rows = sqlx::query_with::<sqlx::Postgres, _>(&sql, vals)
.fetch_all(p)
.await?;
for raw in &raw_rows {
let parent_json = crate::orm::dynamic::decode_pg_to_json(raw, &parent_id_col)?;
let mut obj = serde_json::Map::with_capacity(child_meta.fields.len());
for col in &child_meta.fields {
let v = crate::orm::dynamic::decode_pg_to_json(raw, col)?;
obj.insert(col.name.clone(), v);
}
buckets
.entry(crate::orm::pk_key(&parent_json))
.or_default()
.push(JsonValue::Object(obj));
}
}
}
for row in rows.iter_mut() {
let bucket = match row.pk_as_json() {
Some(pk) => buckets.remove(&crate::orm::pk_key(&pk)).unwrap_or_default(),
None => Vec::new(),
};
row.set_m2m_resolved_json(field_name.as_str(), bucket);
}
}
Ok(())
}
pub(crate) async fn fetch_related_as_json_by_pk(
table: &str,
pk_col: &str,
pk_ty: crate::orm::SqlType,
soft_delete: bool,
ids: &[JsonValue],
pool: &DbPool,
) -> Result<Vec<JsonValue>, sqlx::Error> {
if ids.is_empty() {
return Ok(vec![]);
}
let mut seavals: Vec<sea_query::Value> = Vec::with_capacity(ids.len());
for id in ids {
if id.is_null() {
continue;
}
if let Ok(v) = crate::orm::write::json_to_sea_value(pk_ty, id, false, pk_col, None) {
seavals.push(v);
}
}
if seavals.is_empty() {
return Ok(vec![]);
}
let mut q = sea_query::Query::select();
q.column(sea_query::Asterisk)
.from(crate::db::router::schema_qualified_table(table))
.and_where(
sea_query::Expr::col(sea_query::Alias::new(pk_col))
.is_in(seavals.into_iter().map(sea_query::SimpleExpr::Value)),
);
if soft_delete {
q.and_where(sea_query::Expr::col(sea_query::Alias::new("deleted_at")).is_null());
}
match pool {
DbPool::Sqlite(pool) => {
let (sql, args) = q.build_sqlx(SqliteQueryBuilder);
let rows = sqlx::query_with::<sqlx::Sqlite, _>(&sql, args)
.fetch_all(pool)
.await?;
Ok(rows.iter().map(backend_sqlite::row_to_json).collect())
}
DbPool::Postgres(pool) => {
let (sql, args) = q.build_sqlx(PostgresQueryBuilder);
let rows = sqlx::query_with::<sqlx::Postgres, _>(&sql, args)
.fetch_all(pool)
.await?;
Ok(rows.iter().map(backend_pg::row_to_json).collect())
}
}
}