1use std::collections::{BTreeMap, BTreeSet};
4
5use khive_types::{Entity, Id128, Link, Note, NoteStatus};
6
7use crate::{
8 ChangeSet, CreateOp, CreateTarget, DeleteOp, DeletePreimage, EdgePatch, EdgePreimage,
9 EntityPatch, EntityPreimage, Envelope, LinkOp, NotePatch, NotePreimage, Op, RestoreOp,
10 UpdateOp, UpdatePatch, UpdatePreimage,
11};
12
13#[derive(Clone, Debug, Default)]
22pub struct CurrentState {
23 pub entities: BTreeMap<Id128, Entity>,
25 pub notes: BTreeMap<Id128, Note>,
27 pub edges: BTreeMap<Id128, Link>,
29}
30
31#[derive(Debug, thiserror::Error)]
35pub enum InvertError {
36 #[error("the inverse envelope must differ from the forward envelope")]
38 ReusedEnvelope,
39 #[error("operation {index} ({op}) is not invertible")]
41 NotInvertible {
42 index: usize,
44 op: &'static str,
46 },
47 #[error("operation {index} is missing current record {id}")]
49 MissingCurrent {
50 index: usize,
52 id: Id128,
54 },
55 #[error("operation {index} record {id} differs in field {field}")]
57 Invalidated {
58 index: usize,
60 id: Id128,
62 field: &'static str,
64 },
65 #[error("operation {index} inverse restore is invalid: {reason}")]
67 InverseRestore {
68 index: usize,
70 reason: String,
72 },
73 #[error("operation {index} inverse update is invalid: {reason}")]
75 InverseUpdate {
76 index: usize,
78 reason: String,
80 },
81}
82
83impl ChangeSet {
84 pub fn invert(
101 &self,
102 envelope: Envelope,
103 current: &CurrentState,
104 ) -> Result<ChangeSet, InvertError> {
105 if envelope == self.envelope {
106 return Err(InvertError::ReusedEnvelope);
107 }
108 let mut working = current.clone();
109 let mut tombstones = BTreeSet::new();
112 let mut ops = Vec::with_capacity(self.ops.len());
113 for (index, op) in self.ops.iter().enumerate().rev() {
114 let inverse = match op {
115 Op::Create(create) => {
116 let substrate = match &create.target {
117 CreateTarget::Entity(_) => 0,
118 CreateTarget::Note(_) => 1,
119 };
120 check(
121 index,
122 create.id,
123 "deleted_at",
124 !tombstones.contains(&(substrate, create.id)),
125 )?;
126 invert_create(index, create, &mut working)?
127 }
128 Op::Link(link) => {
129 check(
130 index,
131 link.id,
132 "deleted_at",
133 !tombstones.contains(&(2, link.id)),
134 )?;
135 invert_link(index, link, &mut working)?
136 }
137 Op::Update(update) => invert_update(index, update, &mut working)?,
138 Op::Delete(delete) if delete.hard => {
139 return Err(InvertError::NotInvertible {
140 index,
141 op: "delete",
142 });
143 }
144 Op::Delete(delete) => invert_delete(index, delete, &mut working, &mut tombstones)?,
145 Op::Restore(restore) => {
146 invert_restore(index, restore, &mut working, &mut tombstones)?
147 }
148 Op::Merge(_) => {
149 return Err(InvertError::NotInvertible { index, op: "merge" });
150 }
151 };
152 ops.push(inverse);
153 }
154 Ok(ChangeSet::new(envelope, ops))
155 }
156}
157
158fn check(index: usize, id: Id128, field: &'static str, matches: bool) -> Result<(), InvertError> {
159 if matches {
160 Ok(())
161 } else {
162 Err(InvertError::Invalidated { index, id, field })
163 }
164}
165
166type RecordKey = (u8, Id128);
167
168fn record_key(record: &DeletePreimage) -> RecordKey {
169 match record {
170 DeletePreimage::Entity(row) => (0, row.header.id),
171 DeletePreimage::Note(row) => (1, row.header.id),
172 DeletePreimage::Edge(row) => (2, row.id),
173 }
174}
175
176fn is_deleted(record: &DeletePreimage) -> bool {
177 match record {
178 DeletePreimage::Entity(row) => row.deleted_at.is_some(),
179 DeletePreimage::Note(row) => row.deleted_at.is_some(),
180 DeletePreimage::Edge(row) => row.deleted_at.is_some(),
181 }
182}
183
184fn current_record(
185 index: usize,
186 id: Id128,
187 expected: &DeletePreimage,
188 working: &CurrentState,
189) -> Result<DeletePreimage, InvertError> {
190 let record = match expected {
191 DeletePreimage::Entity(_) => working
192 .entities
193 .get(&id)
194 .cloned()
195 .map(Box::new)
196 .map(DeletePreimage::Entity),
197 DeletePreimage::Note(_) => working
198 .notes
199 .get(&id)
200 .cloned()
201 .map(Box::new)
202 .map(DeletePreimage::Note),
203 DeletePreimage::Edge(_) => working
204 .edges
205 .get(&id)
206 .cloned()
207 .map(Box::new)
208 .map(DeletePreimage::Edge),
209 }
210 .ok_or(InvertError::MissingCurrent { index, id })?;
211 check(index, id, "id", record_key(&record).1 == id)?;
212 Ok(record)
213}
214
215fn rewind_record(record: &DeletePreimage, working: &mut CurrentState) {
216 match record {
217 DeletePreimage::Entity(row) => {
218 working.entities.insert(row.header.id, *row.clone());
219 }
220 DeletePreimage::Note(row) => {
221 working.notes.insert(row.header.id, *row.clone());
222 }
223 DeletePreimage::Edge(row) => {
224 working.edges.insert(row.id, *row.clone());
225 }
226 }
227}
228
229fn check_record_content(
230 index: usize,
231 id: Id128,
232 current: &DeletePreimage,
233 expected: &DeletePreimage,
234) -> Result<(), InvertError> {
235 macro_rules! fields {
236 ($current:ident, $expected:ident, $($field:ident),+ $(,)?) => {
237 $(check(index, id, stringify!($field), $current.$field == $expected.$field)?;)+
238 };
239 }
240 match (current, expected) {
241 (DeletePreimage::Entity(row), DeletePreimage::Entity(prior)) => {
242 fields!(
243 row,
244 prior,
245 kind,
246 entity_type,
247 name,
248 description,
249 properties,
250 tags
251 );
252 check(
253 index,
254 id,
255 "namespace",
256 row.header.namespace == prior.header.namespace,
257 )?;
258 check(
259 index,
260 id,
261 "created_at",
262 row.header.created_at == prior.header.created_at,
263 )?;
264 }
265 (DeletePreimage::Note(row), DeletePreimage::Note(prior)) => {
266 fields!(
267 row,
268 prior,
269 kind,
270 status,
271 content,
272 properties,
273 tags,
274 salience,
275 decay_factor,
276 expires_at
277 );
278 check(
279 index,
280 id,
281 "namespace",
282 row.header.namespace == prior.header.namespace,
283 )?;
284 check(
285 index,
286 id,
287 "created_at",
288 row.header.created_at == prior.header.created_at,
289 )?;
290 }
291 (DeletePreimage::Edge(row), DeletePreimage::Edge(prior)) => {
292 fields!(
293 row, prior, namespace, source, target, relation, weight, properties, created_at
294 );
295 }
296 _ => check(index, id, "substrate", false)?,
297 }
298 Ok(())
299}
300
301fn invert_delete(
302 index: usize,
303 delete: &DeleteOp,
304 working: &mut CurrentState,
305 tombstones: &mut BTreeSet<RecordKey>,
306) -> Result<Op, InvertError> {
307 let id = delete.target_id;
308 let restore = RestoreOp::new(id, delete.preimage.clone())
309 .map_err(|reason| InvertError::InverseRestore { index, reason })?;
310 let mut current = current_record(index, id, &delete.preimage, working)?;
311 let key = record_key(&delete.preimage);
312 check(
313 index,
314 id,
315 "deleted_at",
316 is_deleted(¤t) || tombstones.contains(&key),
317 )?;
318 if !tombstones.contains(&key) {
319 match (&mut current, &delete.preimage) {
320 (DeletePreimage::Entity(row), DeletePreimage::Entity(prior)) => {
321 check(
322 index,
323 id,
324 "updated_at",
325 row.header.updated_at == prior.header.updated_at,
326 )?;
327 }
328 (DeletePreimage::Note(row), DeletePreimage::Note(prior)) => {
329 check(
330 index,
331 id,
332 "updated_at",
333 row.header.updated_at == prior.header.updated_at,
334 )?;
335 check(index, id, "status", row.status == NoteStatus::Deleted)?;
336 row.status = prior.status;
337 }
338 _ => {}
339 }
340 }
341 check_record_content(index, id, ¤t, &delete.preimage)?;
342 rewind_record(&delete.preimage, working);
343 tombstones.remove(&key);
344 Ok(Op::Restore(restore))
345}
346
347fn invert_restore(
348 index: usize,
349 restore: &RestoreOp,
350 working: &mut CurrentState,
351 tombstones: &mut BTreeSet<RecordKey>,
352) -> Result<Op, InvertError> {
353 let id = restore.target_id();
354 let key = record_key(restore.record());
355 let current = current_record(index, id, restore.record(), working)?;
356 check(
357 index,
358 id,
359 "deleted_at",
360 !is_deleted(¤t) && !tombstones.contains(&key),
361 )?;
362 check_record_content(index, id, ¤t, restore.record())?;
363 tombstones.insert(key);
364 Ok(Op::Delete(DeleteOp {
365 target_id: id,
366 hard: false,
367 preimage: current,
368 }))
369}
370
371fn invert_create(
372 index: usize,
373 create: &CreateOp,
374 working: &mut CurrentState,
375) -> Result<Op, InvertError> {
376 let id = create.id;
377 let preimage = match &create.target {
378 CreateTarget::Entity(fields) => {
379 let row = working
380 .entities
381 .get(&id)
382 .ok_or(InvertError::MissingCurrent { index, id })?;
383 check(index, id, "id", row.header.id == id)?;
384 check(
385 index,
386 id,
387 "namespace",
388 row.header.namespace == create.namespace,
389 )?;
390 check(index, id, "deleted_at", row.deleted_at.is_none())?;
391 check(index, id, "kind", row.kind == fields.entity_kind)?;
392 check(
393 index,
394 id,
395 "entity_type",
396 row.entity_type == fields.entity_type,
397 )?;
398 check(index, id, "name", row.name == fields.name)?;
399 check(
400 index,
401 id,
402 "description",
403 row.description == fields.description,
404 )?;
405 check(index, id, "properties", row.properties == fields.properties)?;
406 check(index, id, "tags", row.tags == fields.tags)?;
407 let captured = row.clone();
408 working.entities.remove(&id);
409 DeletePreimage::Entity(Box::new(captured))
410 }
411 CreateTarget::Note(fields) => {
412 let row = working
413 .notes
414 .get(&id)
415 .ok_or(InvertError::MissingCurrent { index, id })?;
416 check(index, id, "id", row.header.id == id)?;
417 check(
418 index,
419 id,
420 "namespace",
421 row.header.namespace == create.namespace,
422 )?;
423 check(index, id, "deleted_at", row.deleted_at.is_none())?;
424 check(index, id, "kind", row.kind == fields.note_kind)?;
425 check(index, id, "status", row.status == NoteStatus::default())?;
426 check(index, id, "expires_at", row.expires_at.is_none())?;
427 check(index, id, "content", row.content == fields.content)?;
428 check(index, id, "properties", row.properties == fields.properties)?;
429 check(index, id, "tags", row.tags == fields.tags)?;
430 check(index, id, "salience", row.salience == fields.salience)?;
431 check(
432 index,
433 id,
434 "decay_factor",
435 row.decay_factor == fields.decay_factor,
436 )?;
437 let captured = row.clone();
438 working.notes.remove(&id);
439 DeletePreimage::Note(Box::new(captured))
440 }
441 };
442 Ok(Op::Delete(DeleteOp {
443 target_id: id,
444 hard: false,
445 preimage,
446 }))
447}
448
449fn invert_link(index: usize, link: &LinkOp, working: &mut CurrentState) -> Result<Op, InvertError> {
450 let id = link.id;
451 let row = working
452 .edges
453 .get(&id)
454 .ok_or(InvertError::MissingCurrent { index, id })?;
455 check(index, id, "id", row.id == id)?;
456 check(
457 index,
458 id,
459 "namespace",
460 row.namespace == link.namespace.as_str(),
461 )?;
462 check(index, id, "deleted_at", row.deleted_at.is_none())?;
463 check(index, id, "source", row.source == link.source)?;
464 check(index, id, "target", row.target == link.target)?;
465 check(index, id, "relation", row.relation == link.relation)?;
466 check(index, id, "weight", row.weight == link.weight)?;
467 check(index, id, "properties", row.properties == link.properties)?;
468 let captured = row.clone();
469 working.edges.remove(&id);
470 Ok(Op::Delete(DeleteOp {
471 target_id: id,
472 hard: false,
473 preimage: DeletePreimage::Edge(Box::new(captured)),
474 }))
475}
476
477fn invert_update(
478 index: usize,
479 update: &UpdateOp,
480 working: &mut CurrentState,
481) -> Result<Op, InvertError> {
482 let id = update.target_id();
483 match (update.patch(), update.preimage()) {
484 (UpdatePatch::Entity(patch), UpdatePreimage::Entity(prior)) => {
485 let row = working
486 .entities
487 .get_mut(&id)
488 .ok_or(InvertError::MissingCurrent { index, id })?;
489 check(index, id, "id", row.header.id == id)?;
490 if let Some(value) = &patch.name {
491 check(index, id, "name", &row.name == value)?;
492 }
493 if let Some(value) = &patch.description {
494 check(
495 index,
496 id,
497 "description",
498 row.description.as_ref() == value.as_ref(),
499 )?;
500 }
501 if let Some(value) = &patch.entity_type {
502 check(
503 index,
504 id,
505 "entity_type",
506 row.entity_type.as_ref() == value.as_ref(),
507 )?;
508 }
509 if let Some(value) = &patch.properties {
510 check(index, id, "properties", &row.properties == value)?;
511 }
512 if let Some(value) = &patch.tags {
513 check(index, id, "tags", &row.tags == value)?;
514 }
515 let inverse = swapped_update(index, update)?;
516 if let Some(value) = &prior.name {
517 row.name = value.clone();
518 }
519 if let Some(value) = &prior.description {
520 row.description = value.clone();
521 }
522 if let Some(value) = &prior.entity_type {
523 row.entity_type = value.clone();
524 }
525 if let Some(value) = &prior.properties {
526 row.properties = value.clone();
527 }
528 if let Some(value) = &prior.tags {
529 row.tags = value.clone();
530 }
531 Ok(Op::Update(inverse))
532 }
533 (UpdatePatch::Note(patch), UpdatePreimage::Note(prior)) => {
534 let row = working
535 .notes
536 .get_mut(&id)
537 .ok_or(InvertError::MissingCurrent { index, id })?;
538 check(index, id, "id", row.header.id == id)?;
539 if let Some(value) = &patch.content {
540 check(index, id, "content", &row.content == value)?;
541 }
542 if let Some(value) = patch.salience {
543 check(index, id, "salience", row.salience == value)?;
544 }
545 if let Some(value) = patch.decay_factor {
546 check(index, id, "decay_factor", row.decay_factor == value)?;
547 }
548 if let Some(value) = &patch.properties {
549 check(index, id, "properties", &row.properties == value)?;
550 }
551 if let Some(value) = &patch.tags {
552 check(index, id, "tags", &row.tags == value)?;
553 }
554 let inverse = swapped_update(index, update)?;
555 if let Some(value) = &prior.content {
556 row.content = value.clone();
557 }
558 if let Some(value) = prior.salience {
559 row.salience = value;
560 }
561 if let Some(value) = prior.decay_factor {
562 row.decay_factor = value;
563 }
564 if let Some(value) = &prior.properties {
565 row.properties = value.clone();
566 }
567 if let Some(value) = &prior.tags {
568 row.tags = value.clone();
569 }
570 Ok(Op::Update(inverse))
571 }
572 (UpdatePatch::Edge(patch), UpdatePreimage::Edge(prior)) => {
573 let row = working
574 .edges
575 .get_mut(&id)
576 .ok_or(InvertError::MissingCurrent { index, id })?;
577 check(index, id, "id", row.id == id)?;
578 if let Some(value) = patch.relation {
579 check(index, id, "relation", row.relation == value)?;
580 }
581 if let Some(value) = patch.weight {
582 check(index, id, "weight", row.weight == value)?;
583 }
584 let inverse = swapped_update(index, update)?;
585 if let Some(value) = prior.relation {
586 row.relation = value;
587 }
588 if let Some(value) = prior.weight {
589 row.weight = value;
590 }
591 Ok(Op::Update(inverse))
592 }
593 _ => swapped_update(index, update).map(Op::Update),
596 }
597}
598
599fn swapped_update(index: usize, update: &UpdateOp) -> Result<UpdateOp, InvertError> {
600 let patch = match update.preimage() {
601 UpdatePreimage::Entity(prior) => UpdatePatch::Entity(EntityPatch {
602 name: prior.name.clone(),
603 description: prior.description.clone(),
604 properties: prior.properties.clone(),
605 tags: prior.tags.clone(),
606 entity_type: prior.entity_type.clone(),
607 }),
608 UpdatePreimage::Note(prior) => UpdatePatch::Note(NotePatch {
609 content: prior.content.clone(),
610 salience: prior.salience,
611 decay_factor: prior.decay_factor,
612 properties: prior.properties.clone(),
613 tags: prior.tags.clone(),
614 }),
615 UpdatePreimage::Edge(prior) => UpdatePatch::Edge(EdgePatch {
616 relation: prior.relation,
617 weight: prior.weight,
618 }),
619 };
620 let preimage = match update.patch() {
621 UpdatePatch::Entity(patch) => UpdatePreimage::Entity(EntityPreimage {
622 name: patch.name.clone(),
623 description: patch.description.clone(),
624 properties: patch.properties.clone(),
625 tags: patch.tags.clone(),
626 entity_type: patch.entity_type.clone(),
627 }),
628 UpdatePatch::Note(patch) => UpdatePreimage::Note(NotePreimage {
629 content: patch.content.clone(),
630 salience: patch.salience,
631 decay_factor: patch.decay_factor,
632 properties: patch.properties.clone(),
633 tags: patch.tags.clone(),
634 }),
635 UpdatePatch::Edge(patch) => UpdatePreimage::Edge(EdgePreimage {
636 relation: patch.relation,
637 weight: patch.weight,
638 }),
639 };
640 UpdateOp::new(update.target_id(), patch, preimage)
641 .map_err(|reason| InvertError::InverseUpdate { index, reason })
642}