use crate::entity::{
EntityState, IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter,
};
use crate::error::EFResult;
use crate::provider::{DbValue, IDatabaseProvider};
use crate::query::{BoolExpr, IQueryable, QueryBuilder};
use std::collections::HashMap;
use std::sync::Arc;
pub trait IDbSet<T: IEntityType>: IQueryable<T> + Send + Sync {
fn add(&mut self, entity: T);
fn remove_all(&mut self);
fn remove_at(&mut self, index: usize) -> EFResult<()>;
fn attach(&mut self, entity: T);
fn added_entities(&self) -> Vec<&T>;
fn modified_entities(&self) -> Vec<&T>;
fn deleted_entities(&self) -> Vec<&T>;
fn entries_with_state(&self) -> Vec<(&T, EntityState)>;
fn clear_entries(&mut self);
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
}
pub struct DbSet<T: IEntityType> {
pub(crate) entries: Vec<TrackedEntry<T>>,
table_name: String,
provider: Option<Arc<dyn IDatabaseProvider>>,
query_filter: Option<BoolExpr>,
filter_map: Option<Arc<HashMap<String, crate::query::CompiledFilter>>>,
lazy_loading_enabled: bool,
}
pub struct TrackedEntry<T: IEntityType> {
pub entity: T,
pub state: EntityState,
pub original: Option<HashMap<String, DbValue>>,
pub modified_properties: Vec<String>,
pub is_upsert: bool,
}
impl<T: IEntityType + IEntitySnapshot> DbSet<T> {
pub fn new(table_name: impl Into<String>) -> Self {
Self {
entries: Vec::new(),
table_name: table_name.into(),
provider: None,
query_filter: None,
filter_map: None,
lazy_loading_enabled: false,
}
}
pub fn with_provider(
table_name: impl Into<String>,
provider: Arc<dyn IDatabaseProvider>,
) -> Self {
Self {
entries: Vec::new(),
table_name: table_name.into(),
provider: Some(provider),
query_filter: None,
filter_map: None,
lazy_loading_enabled: false,
}
}
pub fn set_query_filter(&mut self, filter: BoolExpr) {
self.query_filter = Some(filter);
}
pub fn set_filter_map(&mut self, map: Arc<HashMap<String, crate::query::CompiledFilter>>) {
self.filter_map = Some(map);
}
pub(crate) fn set_lazy_loading_enabled(&mut self, enabled: bool) {
self.lazy_loading_enabled = enabled;
}
pub(crate) fn query_filter(&self) -> Option<&BoolExpr> {
self.query_filter.as_ref()
}
pub fn set_provider(&mut self, provider: Arc<dyn IDatabaseProvider>) {
self.provider = Some(provider);
}
pub fn add(&mut self, entity: T) {
IDbSet::add(self, entity);
}
pub fn remove_all(&mut self) {
IDbSet::remove_all(self);
}
pub fn remove_at(&mut self, index: usize) -> EFResult<()> {
IDbSet::remove_at(self, index)
}
pub fn clear_entries(&mut self) {
IDbSet::clear_entries(self);
}
pub fn len(&self) -> usize {
IDbSet::len(self)
}
pub fn is_empty(&self) -> bool {
IDbSet::is_empty(self)
}
pub fn query(&self) -> QueryBuilder<T> {
IQueryable::query(self)
}
pub fn query_ignore_filters(&self) -> QueryBuilder<T> {
let qb = match &self.provider {
Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
None => QueryBuilder::new(&self.table_name),
};
qb.with_filter_map(self.filter_map.clone())
.with_lazy_loading(self.lazy_loading_enabled)
}
pub fn attach(&mut self, entity: T) {
IDbSet::attach(self, entity);
}
pub fn tracked_entries(&self) -> impl Iterator<Item = &T> {
self.entries.iter().map(|e| &e.entity)
}
pub fn tracked_entries_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.entries.iter_mut().map(|e| &mut e.entity)
}
pub fn retain(&mut self, f: impl FnMut(&TrackedEntry<T>) -> bool) {
self.entries.retain(f);
}
pub fn remove_range(&mut self, entities: &[T])
where
T: PartialEq,
{
for entity in entities {
if let Some(entry) = self.entries.iter_mut().find(|e| e.entity == *entity) {
entry.state = EntityState::Deleted;
}
}
}
pub async fn load_all(&mut self) -> EFResult<()>
where
T: IFromRow
+ INavigationSetter
+ IGetKeyValues
+ IEntitySnapshot
+ crate::entity::ILazyInit,
{
let items = self.query().to_list().await?;
self.clear_entries();
for item in items {
self.attach(item);
}
Ok(())
}
pub fn update(&mut self, entity: T) {
self.entries.push(TrackedEntry {
entity,
state: EntityState::Modified,
original: None,
modified_properties: Vec::new(),
is_upsert: false,
});
}
pub fn upsert(&mut self, entity: T) {
self.entries.push(TrackedEntry {
entity,
state: EntityState::Added,
original: None,
modified_properties: Vec::new(),
is_upsert: true,
});
}
pub fn detect_changes(&mut self)
where
T: IEntitySnapshot,
{
for entry in &mut self.entries {
if entry.state != EntityState::Unchanged {
continue;
}
if let Some(ref original) = entry.original {
let current = entry.entity.snapshot();
if current == *original {
entry.modified_properties.clear();
continue;
}
let changed: Vec<String> = current
.iter()
.filter(|(k, v)| original.get(k.as_str()) != Some(v))
.map(|(k, _)| k.clone())
.collect();
if !changed.is_empty() {
entry.state = EntityState::Modified;
entry.modified_properties = changed;
}
}
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn tracked_by_state(
&self,
state: EntityState,
) -> Vec<(&T, Option<&HashMap<String, DbValue>>, &[String], bool)> {
self.entries
.iter()
.filter(|e| e.state == state)
.map(|e| {
(
&e.entity,
e.original.as_ref(),
e.modified_properties.as_slice(),
e.is_upsert,
)
})
.collect()
}
pub(crate) fn backfill_added_keys(&mut self, keys: &[i64])
where
T: IGetKeyValues,
{
let mut idx = 0usize;
for entry in &mut self.entries {
if entry.state != EntityState::Added || entry.is_upsert {
continue;
}
if let Some(&key) = keys.get(idx) {
if key != 0 {
entry.entity.set_auto_increment_key(key);
}
}
idx += 1;
}
}
pub(crate) fn accept_all_changes(&mut self)
where
T: IEntitySnapshot,
{
self.entries.retain(|e| e.state != EntityState::Deleted);
for entry in &mut self.entries {
if entry.state == EntityState::Added || entry.state == EntityState::Modified {
entry.state = EntityState::Unchanged;
entry.original = Some(entry.entity.snapshot());
entry.modified_properties.clear();
}
}
}
pub async fn exists_by_id(&self, key_values: HashMap<String, DbValue>) -> EFResult<bool>
where
T: IFromRow
+ INavigationSetter
+ IGetKeyValues
+ IEntitySnapshot
+ crate::entity::ILazyInit,
{
let pairs: Vec<(&str, DbValue)> = key_values
.iter()
.map(|(k, v)| (k.as_str(), v.clone()))
.collect();
Ok(self.query().find_by_key(&pairs).await?.is_some())
}
pub fn filter<F>(&self, apply: F) -> QueryBuilder<T>
where
F: FnOnce(QueryBuilder<T>) -> QueryBuilder<T>,
{
apply(self.query())
}
}
impl<T: IEntityType> IQueryable<T> for DbSet<T> {
fn query(&self) -> QueryBuilder<T> {
let mut qb = match &self.provider {
Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
None => QueryBuilder::new(&self.table_name),
};
if let Some(ref filter) = self.query_filter {
qb = qb.apply_query_filter(filter.clone());
}
qb.with_filter_map(self.filter_map.clone())
.with_lazy_loading(self.lazy_loading_enabled)
}
}
impl<T: IEntityType + IEntitySnapshot> IDbSet<T> for DbSet<T> {
fn add(&mut self, entity: T) {
self.entries.push(TrackedEntry {
entity,
state: EntityState::Added,
original: None,
modified_properties: Vec::new(),
is_upsert: false,
});
}
fn remove_all(&mut self) {
for entry in &mut self.entries {
entry.state = EntityState::Deleted;
}
}
fn remove_at(&mut self, index: usize) -> EFResult<()> {
if let Some(entry) = self.entries.get_mut(index) {
entry.state = EntityState::Deleted;
Ok(())
} else {
Err(crate::error::EFError::not_found(
"Entity not found at the given index".to_string(),
))
}
}
fn attach(&mut self, entity: T) {
let original = entity.snapshot();
self.entries.push(TrackedEntry {
entity,
state: EntityState::Unchanged,
original: Some(original),
modified_properties: Vec::new(),
is_upsert: false,
});
}
fn added_entities(&self) -> Vec<&T> {
self.entries
.iter()
.filter(|e| e.state == EntityState::Added)
.map(|e| &e.entity)
.collect()
}
fn modified_entities(&self) -> Vec<&T> {
self.entries
.iter()
.filter(|e| e.state == EntityState::Modified)
.map(|e| &e.entity)
.collect()
}
fn deleted_entities(&self) -> Vec<&T> {
self.entries
.iter()
.filter(|e| e.state == EntityState::Deleted)
.map(|e| &e.entity)
.collect()
}
fn entries_with_state(&self) -> Vec<(&T, EntityState)> {
self.entries.iter().map(|e| (&e.entity, e.state)).collect()
}
fn clear_entries(&mut self) {
self.entries.clear();
}
fn len(&self) -> usize {
self.entries.len()
}
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}