use crate::entity::IEntityType;
use crate::error::{EFError, EFResult};
use crate::metadata::{NavigationKind, NavigationMeta};
use crate::provider::{DbValue, IDatabaseProvider, ISqlGenerator};
use crate::query::CompiledFilter;
use std::collections::HashMap;
use std::sync::Arc;
pub const MAX_LAZY_DEPTH: usize = 16;
pub trait LazyContext: Send + Sync {
fn provider(&self) -> &Arc<dyn IDatabaseProvider>;
fn owner_snapshot(&self) -> &HashMap<String, DbValue>;
fn owner_key_values(&self) -> &HashMap<String, DbValue>;
fn navigation(&self) -> &NavigationMeta;
fn filter_map(&self) -> Option<&HashMap<String, CompiledFilter>>;
fn depth(&self) -> usize;
}
pub struct LazyContextImpl {
provider: Arc<dyn IDatabaseProvider>,
owner_snapshot: HashMap<String, DbValue>,
owner_key_values: HashMap<String, DbValue>,
navigation: NavigationMeta,
filter_map: Option<Arc<HashMap<String, CompiledFilter>>>,
depth: usize,
}
impl LazyContextImpl {
pub fn new(
provider: Arc<dyn IDatabaseProvider>,
owner_snapshot: HashMap<String, DbValue>,
owner_key_values: HashMap<String, DbValue>,
navigation: NavigationMeta,
filter_map: Option<Arc<HashMap<String, CompiledFilter>>>,
depth: usize,
) -> Self {
Self {
provider,
owner_snapshot,
owner_key_values,
navigation,
filter_map,
depth,
}
}
}
impl LazyContext for LazyContextImpl {
fn provider(&self) -> &Arc<dyn IDatabaseProvider> {
&self.provider
}
fn owner_snapshot(&self) -> &HashMap<String, DbValue> {
&self.owner_snapshot
}
fn owner_key_values(&self) -> &HashMap<String, DbValue> {
&self.owner_key_values
}
fn navigation(&self) -> &NavigationMeta {
&self.navigation
}
fn filter_map(&self) -> Option<&HashMap<String, CompiledFilter>> {
self.filter_map.as_deref()
}
fn depth(&self) -> usize {
self.depth
}
}
fn build_lazy_query(
nav: &NavigationMeta,
owner_snapshot: &HashMap<String, DbValue>,
owner_key_values: &HashMap<String, DbValue>,
gen: &dyn ISqlGenerator,
filter_map: Option<&HashMap<String, CompiledFilter>>,
) -> EFResult<Option<(String, Vec<DbValue>)>> {
let Some(related_table) = nav.related_table.as_deref() else {
return Ok(None);
};
let Some(fk_column) = nav.fk_column.as_deref() else {
return Ok(None);
};
let Some(ref_column) = nav.referenced_key_column.as_deref() else {
return Ok(None);
};
match nav.kind {
NavigationKind::HasMany | NavigationKind::HasOne => {
let Some(owner_pk) = owner_key_values.get(ref_column) else {
return Ok(None);
};
let placeholder = gen.parameter_placeholder(1);
let mut sql = format!(
"SELECT * FROM {} WHERE {} = {}",
related_table, fk_column, placeholder
);
let mut params = vec![owner_pk.clone()];
apply_filter(&mut sql, &mut params, related_table, filter_map, gen);
if nav.kind == NavigationKind::HasOne {
sql.push_str(" LIMIT 1");
}
Ok(Some((sql, params)))
}
NavigationKind::BelongsTo => {
let Some(owner_fk) = owner_snapshot.get(fk_column) else {
return Ok(None);
};
if matches!(owner_fk, DbValue::Null) {
return Ok(None);
}
let placeholder = gen.parameter_placeholder(1);
let mut sql = format!(
"SELECT * FROM {} WHERE {} = {}",
related_table, ref_column, placeholder
);
let mut params = vec![owner_fk.clone()];
apply_filter(&mut sql, &mut params, related_table, filter_map, gen);
Ok(Some((sql, params)))
}
NavigationKind::ManyToMany => {
Ok(None)
}
}
}
fn build_m2m_lazy_queries(
nav: &NavigationMeta,
owner_key_values: &HashMap<String, DbValue>,
ref_column: &str,
gen: &dyn ISqlGenerator,
filter_map: Option<&HashMap<String, CompiledFilter>>,
) -> EFResult<Option<(String, Vec<DbValue>, String)>> {
let Some(join_table) = nav.through_table.as_deref() else {
return Ok(None);
};
let Some(parent_fk) = nav.through_parent_fk.as_deref() else {
return Ok(None);
};
let Some(related_fk) = nav.through_related_fk.as_deref() else {
return Ok(None);
};
let Some(related_table) = nav.related_table.as_deref() else {
return Ok(None);
};
let Some(owner_pk) = owner_key_values.get(ref_column) else {
return Ok(None);
};
let placeholder = gen.parameter_placeholder(1);
let join_sql = format!(
"SELECT {} FROM {} WHERE {} = {}",
related_fk, join_table, parent_fk, placeholder
);
let join_params = vec![owner_pk.clone()];
let _ = filter_map; Ok(Some((join_sql, join_params, related_table.to_string())))
}
fn apply_filter(
sql: &mut String,
params: &mut Vec<DbValue>,
related_table: &str,
filter_map: Option<&HashMap<String, CompiledFilter>>,
gen: &dyn ISqlGenerator,
) {
use crate::query::compile_bool_expr;
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 async fn load_scalar_lazy<T>(ctx: &dyn LazyContext) -> EFResult<Option<T>>
where
T: IEntityType + crate::entity::IFromRow,
{
let nav = ctx.navigation();
if nav.kind == NavigationKind::ManyToMany {
return Err(EFError::other(
"load_scalar_lazy called for ManyToMany navigation",
));
}
let gen = ctx.provider().sql_generator();
let Some((sql, params)) = build_lazy_query(
nav,
ctx.owner_snapshot(),
ctx.owner_key_values(),
gen,
ctx.filter_map(),
)?
else {
return Ok(None);
};
let provider = ctx.provider();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
drop(sql);
drop(params);
if let Some(row) = rows.into_iter().next() {
let entity = T::from_row(&row)?;
Ok(Some(entity))
} else {
Ok(None)
}
}
pub async fn load_collection_lazy<T>(ctx: &dyn LazyContext) -> EFResult<Vec<T>>
where
T: IEntityType + crate::entity::IFromRow,
{
let nav = ctx.navigation();
if nav.kind == NavigationKind::ManyToMany {
return load_m2m_collection_lazy::<T>(ctx).await;
}
let gen = ctx.provider().sql_generator();
let Some((sql, params)) = build_lazy_query(
nav,
ctx.owner_snapshot(),
ctx.owner_key_values(),
gen,
ctx.filter_map(),
)?
else {
return Ok(Vec::new());
};
let provider = ctx.provider();
let mut conn = provider.get_connection().await?;
let rows = conn.query(&sql, ¶ms).await?;
let mut entities = Vec::with_capacity(rows.len());
for row in rows {
entities.push(T::from_row(&row)?);
}
Ok(entities)
}
async fn load_m2m_collection_lazy<T>(ctx: &dyn LazyContext) -> EFResult<Vec<T>>
where
T: IEntityType + crate::entity::IFromRow,
{
let nav = ctx.navigation();
let provider = ctx.provider();
let gen = provider.sql_generator();
let ref_column = nav.referenced_key_column.as_deref().unwrap_or("id");
let Some((join_sql, join_params, related_table)) = build_m2m_lazy_queries(
nav,
ctx.owner_key_values(),
ref_column,
gen,
ctx.filter_map(),
)?
else {
return Ok(Vec::new());
};
let mut conn = provider.get_connection().await?;
let join_rows = conn.query(&join_sql, &join_params).await?;
if join_rows.is_empty() {
return Ok(Vec::new());
}
let related_fk_index = nav.through_related_fk_index;
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let related_ids: Vec<DbValue> = join_rows
.iter()
.filter_map(|row| row.get(related_fk_index).cloned())
.filter(|v| seen.insert(format!("{}", v)))
.collect();
if related_ids.is_empty() {
return Ok(Vec::new());
}
let ref_pk_col = nav
.referenced_key_column
.as_deref()
.map(|s| s.to_string())
.or_else(|| {
nav.related_entity_meta.and_then(|f| {
let meta = f();
meta.primary_keys.first().map(|k| k.to_string())
})
})
.unwrap_or_else(|| "id".to_string());
let placeholders: Vec<String> = (0..related_ids.len())
.map(|i| gen.parameter_placeholder(i + 1))
.collect();
let mut sql = format!(
"SELECT * FROM {} WHERE {} IN ({})",
related_table,
ref_pk_col,
placeholders.join(", ")
);
let mut params: Vec<DbValue> = related_ids;
apply_filter(&mut sql, &mut params, &related_table, ctx.filter_map(), gen);
let rows = conn.query(&sql, ¶ms).await?;
let mut entities = Vec::with_capacity(rows.len());
for row in rows {
entities.push(T::from_row(&row)?);
}
Ok(entities)
}