use ahash::{AHashMap, AHashSet};
use crate::entity::Entity;
use crate::index::ReverseIndex;
use crate::model::Model;
use crate::mutation::conflict::Conflict;
use crate::value::{EntityId, Value};
#[derive(Debug, Clone, PartialEq)]
pub enum Edit {
Create {
id: EntityId,
entity: Entity,
},
SetAttribute {
id: EntityId,
slot: usize,
value: Value,
},
Retype {
id: EntityId,
type_name: std::sync::Arc<str>,
},
Remove {
id: EntityId,
},
}
#[derive(Debug, Clone)]
pub struct Transaction {
revision: u64,
next_id: u64,
edits: Vec<Edit>,
}
impl Transaction {
#[must_use]
pub fn new(model: &Model) -> Self {
Self {
revision: model.revision(),
next_id: model.next_id().0,
edits: Vec::new(),
}
}
#[must_use]
pub fn revision(&self) -> u64 {
self.revision
}
#[must_use]
pub fn edits(&self) -> &[Edit] {
&self.edits
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.edits.len()
}
pub fn create(&mut self, entity: Entity) -> EntityId {
#[cfg(feature = "authored-dump")]
crate::authored_dump::record(&entity.type_name, "create");
let id = EntityId(self.next_id);
self.next_id += 1;
self.edits.push(Edit::Create { id, entity });
id
}
pub fn stage(&mut self, edit: Edit) -> &mut Self {
if let Edit::Create { id, .. } = &edit {
self.next_id = self.next_id.max(id.0 + 1);
}
self.edits.push(edit);
self
}
pub fn set_attribute(&mut self, id: EntityId, slot: usize, value: Value) -> &mut Self {
self.edits.push(Edit::SetAttribute { id, slot, value });
self
}
pub fn retype(&mut self, id: EntityId, type_name: impl Into<std::sync::Arc<str>>) -> &mut Self {
self.edits.push(Edit::Retype {
id,
type_name: type_name.into(),
});
self
}
pub fn remove(&mut self, id: EntityId) -> &mut Self {
self.edits.push(Edit::Remove { id });
self
}
#[must_use]
pub fn preflight(&self, model: &Model) -> Vec<Conflict> {
let mut conflicts = Vec::new();
if model.revision() != self.revision {
conflicts.push(Conflict::StaleRevision {
expected: self.revision,
found: model.revision(),
});
return conflicts;
}
let mut created: AHashMap<EntityId, &Entity> = AHashMap::new();
let mut removed: AHashSet<EntityId> = AHashSet::new();
let mut writes: AHashMap<EntityId, AHashMap<usize, &Value>> = AHashMap::new();
for edit in &self.edits {
match edit {
Edit::Create { id, entity } => {
created.insert(*id, entity);
removed.remove(id);
}
Edit::Remove { id } => {
removed.insert(*id);
}
Edit::SetAttribute { id, slot, value } => {
writes.entry(*id).or_default().insert(*slot, value);
}
Edit::Retype { .. } => {}
}
}
let exists = |id: EntityId| -> bool {
!removed.contains(&id) && (created.contains_key(&id) || model.get(id).is_some())
};
for (index, edit) in self.edits.iter().enumerate() {
match edit {
Edit::Create { id, entity } => {
if model.get(*id).is_some() {
conflicts.push(Conflict::IdAlreadyExists {
edit: index,
id: *id,
});
}
for (slot, attribute) in entity.attributes.iter().enumerate() {
if writes.get(id).is_some_and(|w| w.contains_key(&slot)) {
continue;
}
check_refs(attribute, index, *id, slot, &exists, &mut conflicts);
}
}
Edit::SetAttribute { id, slot, value } => {
if !exists(*id) {
conflicts.push(Conflict::MissingTarget {
edit: index,
id: *id,
});
continue;
}
if writes
.get(id)
.and_then(|w| w.get(slot))
.is_some_and(|winner| !std::ptr::eq(*winner, value))
{
continue;
}
check_refs(value, index, *id, *slot, &exists, &mut conflicts);
}
Edit::Retype { id, .. } => {
if !exists(*id) {
conflicts.push(Conflict::MissingTarget {
edit: index,
id: *id,
});
}
}
Edit::Remove { id } => {
if model.get(*id).is_none() && !created.contains_key(id) {
conflicts.push(Conflict::MissingTarget {
edit: index,
id: *id,
});
}
}
}
}
if self.edits.iter().any(|e| matches!(e, Edit::Remove { .. })) {
let index = ReverseIndex::build(model);
let mut reported: AHashSet<EntityId> = AHashSet::new();
for (position, edit) in self.edits.iter().enumerate() {
let Edit::Remove { id } = edit else { continue };
if !reported.insert(*id) {
continue;
}
for referrer in index.referrers(*id) {
if removed.contains(&referrer.from) {
continue;
}
if writes
.get(&referrer.from)
.is_some_and(|w| w.contains_key(&referrer.slot))
{
continue;
}
conflicts.push(Conflict::RemovalWouldDangle {
edit: position,
removed: *id,
referrer: referrer.from,
slot: referrer.slot,
});
}
}
}
conflicts
}
pub fn commit(self, model: &mut Model) -> Result<Applied, Vec<Conflict>> {
let conflicts = self.preflight(model);
if !conflicts.is_empty() {
return Err(conflicts);
}
let mut applied = Applied {
created: Vec::new(),
removed: Vec::new(),
revision: 0,
};
for edit in self.edits {
match edit {
Edit::Create { id, entity } => {
model.insert(id, entity);
applied.created.push(id);
}
Edit::SetAttribute { id, slot, value } => {
model.set_attribute(id, slot, value);
}
Edit::Retype { id, type_name } => {
model.retype(id, type_name);
}
Edit::Remove { id } => {
if let Some(entity) = model.remove(id) {
applied.removed.push((id, entity));
}
}
}
}
applied.revision = model.revision();
Ok(applied)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Applied {
pub created: Vec<EntityId>,
pub removed: Vec<(EntityId, Entity)>,
pub revision: u64,
}
fn check_refs(
value: &Value,
edit: usize,
from: EntityId,
slot: usize,
exists: &impl Fn(EntityId) -> bool,
conflicts: &mut Vec<Conflict>,
) {
value.for_each_ref(&mut |target| {
if !exists(target) {
conflicts.push(Conflict::DanglingReference {
edit,
from,
slot,
target,
});
}
});
}