use super::impl_dml_update_analysis::*;
use super::*;
use crate::application::entity::AppliedEntityMutation;
use crate::application::ports::entity_row_fields_snapshot;
#[derive(Default)]
pub(super) struct DeleteBatchResult {
pub(super) affected: u64,
pub(super) changes: Vec<(EntityId, u64)>,
pub(super) pre_images: HashMap<u64, crate::json::Value>,
_reservations: Vec<std::rc::Rc<super::memory_admission::OwnedMemoryReservation>>,
}
impl RedDBRuntime {
pub(super) fn delete_entities_batch(
&self,
collection: &str,
ids: &[EntityId],
_topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
filter: Option<&Filter>,
capture_pre_images: bool,
) -> RedDBResult<DeleteBatchResult> {
if ids.is_empty() {
return Ok(DeleteBatchResult::default());
}
let store = self.db().store();
let Some(manager) = store.get_collection(collection) else {
return Ok(DeleteBatchResult::default());
};
let row_locks =
super::delete_publication::lock_delete_rows(self, collection, &manager, ids)?;
let _publication = store.vault_publication_lock.lock();
let active_xid = self.current_xid();
let conn_id = crate::runtime::impl_core::current_connection_id();
let mut preimages = Vec::new();
let mut reservations = super::memory_admission::MutationMemoryReservations::new(self);
let mut physical_delete_ids = Vec::new();
let mut index_delete_fields = Vec::new();
let mut pre_images = HashMap::new();
let snapshot = super::impl_core::capture_current_snapshot().map(|mut context| {
if active_xid.is_none() {
context.snapshot = context.manager.fresh_read_snapshot();
}
context
});
let resolver = super::table_row_mvcc_resolver::TableRowMvccReadResolver::captured(snapshot);
let versioned_collection = self.vcs_is_versioned(collection)?;
for &id in ids {
let Some(mut entity) = manager.try_get(id, usize::MAX)? else {
continue;
};
if active_xid.is_none()
&& matches!(entity.kind, crate::storage::EntityKind::TableRow { .. })
&& resolver.resolve_candidate(&entity).is_none()
{
let Some(current) =
resolver.try_resolve_logical_id(&store, collection, entity.logical_id())?
else {
continue;
};
entity = current;
}
let versioned_graph = versioned_collection
&& matches!(entity.data, EntityData::Node(_) | EntityData::Edge(_));
let versioned_vector =
versioned_collection && matches!(entity.data, EntityData::Vector(_));
if matches!(entity.data, EntityData::Row(_)) || versioned_graph || versioned_vector {
let visible = if matches!(entity.kind, crate::storage::EntityKind::TableRow { .. })
{
resolver.resolve_candidate(&entity).is_some()
} else if versioned_vector {
resolver.resolve_read_candidate(&entity).is_some()
} else {
entity.xmax == 0
};
if !visible || !self.delete_candidate_matches_filter(collection, &entity, filter)? {
continue;
}
let bytes = crate::storage::unified::memory_size::entity_bytes(&entity)
.saturating_add(
crate::storage::unified::memory_size::entity_zone_growth_bytes(&entity),
)
.saturating_mul(if capture_pre_images { 4 } else { 3 })
as u64;
reservations.admit_growth(bytes, "delete")?;
if capture_pre_images {
pre_images.insert(entity.id.raw(), super::mutation::entity_row_json(&entity));
}
preimages.push(entity);
} else {
if !self.delete_candidate_matches_filter(collection, &entity, filter)? {
continue;
}
if capture_pre_images {
reservations.admit_growth(
crate::storage::unified::memory_size::entity_bytes(&entity) as u64,
"delete event",
)?;
pre_images.insert(entity.id.raw(), super::mutation::entity_row_json(&entity));
}
let fields = entity_row_fields_snapshot(&entity);
if !fields.is_empty() {
index_delete_fields.push((id, fields));
}
physical_delete_ids.push(id);
}
}
let mut affected = preimages.len() as u64;
let mut changes = Vec::with_capacity(preimages.len() + physical_delete_ids.len());
let owned_reservations = reservations.into_owned();
if !preimages.is_empty() {
let mut publication = super::delete_publication::DeletePublication::new(
self,
collection,
Arc::clone(&manager),
preimages,
owned_reservations.clone(),
active_xid,
row_locks,
);
let persistence = (|| -> RedDBResult<Vec<UnifiedEntity>> {
let entities = publication.apply()?;
store
.persist_entities_to_pager_wal_only(collection, &entities)
.map_err(|error| RedDBError::Internal(error.to_string()))?;
Ok(entities)
})();
let entities = match persistence {
Ok(entities) => entities,
Err(error) => {
publication.abort()?;
return Err(error);
}
};
let tombstoned_ids: Vec<_> = entities.into_iter().map(|entity| entity.id).collect();
if active_xid.is_some() {
publication.finish_transaction(conn_id);
} else {
publication.finish_autocommit()?;
for id in tombstoned_ids {
changes.push((
id,
self.cdc_emit(
crate::replication::cdc::ChangeOperation::Delete,
collection,
id.raw(),
"entity",
),
));
}
}
}
let deleted_ids = store
.delete_batch(collection, &physical_delete_ids)
.map_err(|error| RedDBError::Internal(error.to_string()))?;
#[cfg(test)]
if !deleted_ids.is_empty() {
self.index_store_ref().mutation_test_hook(
collection,
super::index_store::MutationTestPhase::StoragePublished,
);
}
affected += deleted_ids.len() as u64;
for id in &deleted_ids {
if let Some((_, fields)) = index_delete_fields
.iter()
.find(|(indexed_id, _)| indexed_id == id)
{
self.index_store_ref()
.index_entity_delete(collection, *id, fields)
.map_err(RedDBError::Internal)?;
}
store.context_index().remove_entity(*id);
changes.push((
*id,
self.cdc_emit(
crate::replication::cdc::ChangeOperation::Delete,
collection,
id.raw(),
"entity",
),
));
}
Ok(DeleteBatchResult {
affected,
changes,
pre_images,
_reservations: owned_reservations,
})
}
fn delete_candidate_matches_filter(
&self,
collection: &str,
entity: &UnifiedEntity,
filter: Option<&Filter>,
) -> RedDBResult<bool> {
let Some(filter) = filter else {
return Ok(true);
};
crate::runtime::query_exec::evaluate_entity_filter_result_with_db(
Some(self.db().as_ref()),
entity,
filter,
collection,
collection,
)
}
pub(super) fn flush_update_chunk(
&self,
applied: &[AppliedEntityMutation],
topology_guard: parking_lot::RwLockReadGuard<'_, ()>,
) -> RedDBResult<Vec<u64>> {
if applied.is_empty() {
return Ok(Vec::new());
}
let store = self.db().store();
if applied.iter().any(|item| item.context_index_dirty) {
store.context_index().index_entities(
&applied[0].collection,
applied
.iter()
.filter(|item| item.context_index_dirty)
.map(|item| &item.entity),
);
}
for item in applied {
self.refresh_update_secondary_indexes(item)?;
}
drop(topology_guard);
#[cfg(test)]
self.index_store_ref().mutation_test_hook(
&applied[0].collection,
super::index_store::MutationTestPhase::BeforeEvents,
);
let mut lsns = Vec::with_capacity(applied.len());
for item in applied {
let lsn = self.cdc_emit_prebuilt(
crate::replication::cdc::ChangeOperation::Update,
&item.collection,
&item.entity,
update_cdc_item_kind(self, &item.collection, &item.entity),
item.metadata.as_ref(),
false,
);
lsns.push(lsn);
}
Ok(lsns)
}
pub(super) fn persist_update_chunk(
&self,
applied: &[AppliedEntityMutation],
) -> RedDBResult<()> {
if applied.len() <= 1 || UnifiedStore::deferred_store_wal_capture_active() {
self.persist_applied_entity_mutations(applied)?;
} else {
self.with_deferred_store_wal_for_dml(true, || {
self.persist_applied_entity_mutations(applied)
})?;
}
#[cfg(test)]
if let Some(first) = applied.first() {
self.index_store_ref().mutation_test_hook(
&first.collection,
super::index_store::MutationTestPhase::StoragePublished,
);
}
Ok(())
}
fn refresh_update_secondary_indexes(&self, applied: &AppliedEntityMutation) -> RedDBResult<()> {
if applied.pre_mutation_fields.is_empty() {
return Ok(());
}
let post = entity_row_fields_snapshot(&applied.entity);
if post.is_empty() {
return Ok(());
}
let indexed_cols = self
.index_store_ref()
.indexed_columns_set_with_parents(&applied.collection);
if indexed_cols.is_empty() {
return Ok(());
}
let pre = self
.index_store_ref()
.augment_body_derived_index_fields(&applied.pre_mutation_fields, &indexed_cols);
let post = self
.index_store_ref()
.augment_body_derived_index_fields(&post, &indexed_cols);
if let Some(old_version) = applied.replaced_entity.as_ref() {
let old_index_fields: Vec<(String, reddb_types::Value)> = pre
.iter()
.filter(|(col, _)| indexed_cols.contains(col))
.cloned()
.collect();
let new_index_fields: Vec<(String, reddb_types::Value)> = post
.iter()
.filter(|(col, _)| indexed_cols.contains(col))
.cloned()
.collect();
if !old_index_fields.is_empty() {
self.index_store_ref()
.index_entity_delete(&applied.collection, old_version.id, &old_index_fields)
.map_err(crate::RedDBError::Internal)?;
}
if !new_index_fields.is_empty() {
self.index_store_ref()
.index_entity_insert(&applied.collection, applied.entity.id, &new_index_fields)
.map_err(crate::RedDBError::Internal)?;
}
return Ok(());
}
let damage = crate::application::entity::row_damage_vector(&pre, &post);
if damage
.touched_columns()
.into_iter()
.any(|col| indexed_cols.contains(col))
{
self.index_store_ref()
.index_entity_update(&applied.collection, applied.id, &pre, &post)
.map_err(crate::RedDBError::Internal)?;
}
Ok(())
}
}