use crate::entity::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
use crate::error::{EFError, EFResult};
use crate::metadata::{EntityTypeMeta, NavigationKind, NavigationMeta};
use crate::provider::{DbValue, IDatabaseProvider, ISqlGenerator};
use crate::query::{compile_bool_expr, CompiledFilter, IncludePath};
use std::collections::{HashMap, HashSet};
fn apply_filter_to_sql(
sql: &mut String,
params: &mut Vec<DbValue>,
related_table: &str,
filter_map: Option<&HashMap<String, CompiledFilter>>,
gen: &dyn ISqlGenerator,
) {
if let Some(filter) = filter_map.and_then(|m| m.get(related_table)) {
let mut idx = params.len() + 1;
let filter_sql = compile_bool_expr(&filter.expr, gen, &mut idx);
params.extend(filter.params.iter().cloned());
*sql = format!("{} AND ({})", sql, filter_sql);
}
}
pub fn include_paths_for<T: IEntityType, S: AsRef<str>>(
navigation_names: &[S],
) -> Vec<IncludePath> {
let meta = T::entity_meta();
navigation_names
.iter()
.filter_map(|name| include_path_from_meta(&meta, name.as_ref()))
.collect()
}
fn include_path_from_meta(meta: &EntityTypeMeta, navigation: &str) -> Option<IncludePath> {
let nav_meta = meta.find_navigation(navigation)?;
Some(IncludePath {
navigation: navigation.to_string(),
nested: Vec::new(),
related_table: nav_meta.related_table.as_ref().map(|s| s.to_string()),
foreign_key_column: nav_meta.fk_column.as_ref().map(|s| s.to_string()),
referenced_key_column: nav_meta
.referenced_key_column
.as_ref()
.map(|s| s.to_string()),
})
}
pub async fn load_includes<T>(
entities: &mut [T],
includes: &[IncludePath],
provider: &dyn IDatabaseProvider,
filter_map: Option<&HashMap<String, CompiledFilter>>,
) -> EFResult<()>
where
T: IEntityType + IFromRow + INavigationSetter + IGetKeyValues + IEntitySnapshot,
{
if entities.is_empty() || includes.is_empty() {
return Ok(());
}
let meta = T::entity_meta();
for include in includes {
let nav = match meta.find_navigation(&include.navigation) {
Some(n) => n,
None => continue,
};
if nav.kind == NavigationKind::ManyToMany {
load_many_to_many(entities, &include.navigation, nav, provider, filter_map).await?;
} else {
load_scalar_navigation(entities, include, nav, &meta, provider, filter_map).await?;
}
if !include.nested.is_empty() {
T::load_nested_includes(
entities,
&include.navigation,
&include.nested,
provider,
filter_map,
)
.await?;
}
}
Ok(())
}
async fn load_scalar_navigation<T>(
entities: &mut [T],
include: &IncludePath,
nav: &NavigationMeta,
meta: &EntityTypeMeta,
provider: &dyn IDatabaseProvider,
filter_map: Option<&HashMap<String, CompiledFilter>>,
) -> EFResult<()>
where
T: IEntitySnapshot + INavigationSetter + IGetKeyValues,
{
let related_table = include
.related_table
.as_deref()
.or(nav.related_table.as_deref());
let fk_column = include
.foreign_key_column
.as_deref()
.or(nav.fk_column.as_deref());
let ref_column = include
.referenced_key_column
.as_deref()
.or(nav.referenced_key_column.as_deref())
.or_else(|| meta.primary_keys.first().map(|k| k.as_ref()));
let (Some(related_table), Some(fk_column), Some(ref_column)) =
(related_table, fk_column, ref_column)
else {
return Ok(());
};
match nav.kind {
NavigationKind::HasMany => {
let parent_ids: Vec<DbValue> = entities
.iter()
.filter_map(|e| e.key_values().get(ref_column).cloned())
.collect();
if parent_ids.is_empty() {
return Ok(());
}
let gen = provider.sql_generator();
let placeholders: Vec<String> = (0..parent_ids.len())
.map(|i| gen.parameter_placeholder(i + 1))
.collect();
let mut sql = format!(
"SELECT * FROM {} WHERE {} IN ({})",
related_table,
fk_column,
placeholders.join(", ")
);
let mut params = parent_ids;
apply_filter_to_sql(&mut sql, &mut params, related_table, filter_map, gen);
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
let grouped = group_rows(&rows, nav.fk_row_index);
for entity in entities.iter_mut() {
if let Some(pk) = entity.key_values().get(ref_column) {
let key = db_value_key(pk);
if let Some(child_rows) = grouped.get(&key) {
entity.apply_has_many(&include.navigation, child_rows)?;
}
}
}
}
NavigationKind::BelongsTo | NavigationKind::HasOne => {
let fk_values: Vec<DbValue> = entities
.iter()
.filter_map(|e| e.snapshot().get(fk_column).cloned())
.collect();
if fk_values.is_empty() {
return Ok(());
}
let gen = provider.sql_generator();
let placeholders: Vec<String> = (0..fk_values.len())
.map(|i| gen.parameter_placeholder(i + 1))
.collect();
let mut sql = format!(
"SELECT * FROM {} WHERE {} IN ({})",
related_table,
ref_column,
placeholders.join(", ")
);
let mut params = fk_values;
apply_filter_to_sql(&mut sql, &mut params, related_table, filter_map, gen);
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
let lookup = index_rows(&rows, nav.pk_row_index);
for entity in entities.iter_mut() {
if let Some(fk) = entity.snapshot().get(fk_column) {
let key = db_value_key(fk);
if let Some(related_row) = lookup.get(&key) {
entity.apply_reference(&include.navigation, related_row)?;
}
}
}
}
NavigationKind::ManyToMany => {
return Err(EFError::configuration(
"many-to-many navigations are loaded via load_many_to_many",
));
}
}
Ok(())
}
async fn load_many_to_many<T>(
entities: &mut [T],
navigation: &str,
nav: &NavigationMeta,
provider: &dyn IDatabaseProvider,
filter_map: Option<&HashMap<String, CompiledFilter>>,
) -> EFResult<()>
where
T: IEntityType + INavigationSetter + IGetKeyValues,
{
let meta = T::entity_meta();
let ref_column = meta
.primary_keys
.first()
.map(|k| k.as_ref())
.unwrap_or("id");
let (Some(join_table), Some(parent_fk), Some(_related_fk), Some(related_table)) = (
nav.through_table.as_deref(),
nav.through_parent_fk.as_deref(),
nav.through_related_fk.as_deref(),
nav.related_table.as_deref(),
) else {
return Ok(());
};
let parent_ids: Vec<DbValue> = entities
.iter()
.filter_map(|e| e.key_values().get(ref_column).cloned())
.collect();
if parent_ids.is_empty() {
return Ok(());
}
let gen = provider.sql_generator();
let placeholders: Vec<String> = (0..parent_ids.len())
.map(|i| gen.parameter_placeholder(i + 1))
.collect();
let join_sql = format!(
"SELECT * FROM {} WHERE {} IN ({})",
join_table,
parent_fk,
placeholders.join(", ")
);
let mut conn = provider.get_connection().await?;
let join_rows = conn.query(&join_sql, &parent_ids).await?;
if join_rows.is_empty() {
return Ok(());
}
let parent_to_related = group_join_rows(
&join_rows,
nav.through_parent_fk_index,
nav.through_related_fk_index,
);
let mut seen: HashSet<String> = HashSet::new();
let related_ids: Vec<DbValue> = join_rows
.iter()
.filter_map(|row| row.get(nav.through_related_fk_index).cloned())
.filter(|v| seen.insert(db_value_key(v)))
.collect();
if related_ids.is_empty() {
return Ok(());
}
let related_pk = nav.referenced_key_column.as_deref().unwrap_or("id");
let rel_placeholders: Vec<String> = (0..related_ids.len())
.map(|i| gen.parameter_placeholder(i + 1))
.collect();
let mut related_sql = format!(
"SELECT * FROM {} WHERE {} IN ({})",
related_table,
related_pk,
rel_placeholders.join(", ")
);
let mut related_params = related_ids;
apply_filter_to_sql(
&mut related_sql,
&mut related_params,
related_table,
filter_map,
gen,
);
let related_rows = conn.query(&related_sql, &related_params).await?;
let related_by_pk = index_rows(&related_rows, nav.pk_row_index);
for entity in entities.iter_mut() {
if let Some(pk) = entity.key_values().get(ref_column) {
let parent_key = db_value_key(pk);
if let Some(related_keys) = parent_to_related.get(&parent_key) {
let mut child_rows = Vec::new();
for rk in related_keys {
if let Some(row) = related_by_pk.get(&db_value_key(rk)) {
child_rows.push(row.clone());
}
}
if !child_rows.is_empty() {
entity.apply_has_many(navigation, &child_rows)?;
}
}
}
}
Ok(())
}
fn db_value_key(v: &DbValue) -> String {
format!("{}", v)
}
fn group_rows(rows: &[Vec<DbValue>], fk_index: usize) -> HashMap<String, Vec<Vec<DbValue>>> {
let mut map: HashMap<String, Vec<Vec<DbValue>>> = HashMap::new();
for row in rows {
if let Some(fk_val) = row.get(fk_index) {
map.entry(db_value_key(fk_val))
.or_default()
.push(row.clone());
}
}
map
}
fn group_join_rows(
rows: &[Vec<DbValue>],
parent_index: usize,
related_index: usize,
) -> HashMap<String, Vec<DbValue>> {
let mut map: HashMap<String, Vec<DbValue>> = HashMap::new();
for row in rows {
if let (Some(parent), Some(related)) = (row.get(parent_index), row.get(related_index)) {
map.entry(db_value_key(parent))
.or_default()
.push(related.clone());
}
}
map
}
fn index_rows(rows: &[Vec<DbValue>], pk_index: usize) -> HashMap<String, Vec<DbValue>> {
let mut map = HashMap::new();
for row in rows {
if let Some(pk) = row.get(pk_index) {
map.insert(db_value_key(pk), row.clone());
}
}
map
}