1use std::collections::{HashMap, HashSet, VecDeque};
2use std::fmt;
3use std::io;
4
5use serde_json::Value;
6
7use crate::errors::{
8 HaysonCipherError, HaysonPartitionError, HaysonPendingConflictError, HaysonUniqueViolationError,
9 HaysonValidationError,
10};
11use crate::registry::composite_index::unique_key_of;
12use crate::registry::table_registry::TableRegistry;
13use crate::schema::{
14 partition_of, primary_key_of, validate_record, HaysonRecord, TableSchema, ValidateOptions, UNPARTITIONED,
15};
16use crate::storage::{serialize_entry, LogFile, LogMeta, LogOp, LogSet, Slot};
17
18use super::pending_map::{PendingMap, ALL_FIELDS};
19
20pub trait LogAppender {
21 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>>;
22
23 fn append_lines_in(&mut self, part: &str, lines: &[String]) -> io::Result<Vec<Slot>> {
27 let _ = part;
28 self.append_lines(lines)
29 }
30
31 fn sync(&mut self) -> io::Result<()>;
32}
33
34impl LogAppender for LogFile {
35 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
36 self.append(lines)
37 }
38
39 fn sync(&mut self) -> io::Result<()> {
40 LogFile::sync(self)
41 }
42}
43
44impl LogAppender for LogSet {
45 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
46 self.append(UNPARTITIONED, lines)
47 }
48
49 fn append_lines_in(&mut self, part: &str, lines: &[String]) -> io::Result<Vec<Slot>> {
50 self.append(part, lines)
51 }
52
53 fn sync(&mut self) -> io::Result<()> {
54 LogSet::sync(self)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq)]
59pub enum StageError {
60 Validation(HaysonValidationError),
61 Unique(HaysonUniqueViolationError),
62 Conflict(HaysonPendingConflictError),
63 Partition(HaysonPartitionError),
64}
65
66impl fmt::Display for StageError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 StageError::Validation(e) => e.fmt(f),
70 StageError::Unique(e) => e.fmt(f),
71 StageError::Conflict(e) => e.fmt(f),
72 StageError::Partition(e) => e.fmt(f),
73 }
74 }
75}
76
77impl std::error::Error for StageError {}
78
79#[derive(Debug)]
82pub enum CommitError {
83 Io(io::Error),
84 Cipher(HaysonCipherError),
85}
86
87impl fmt::Display for CommitError {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 CommitError::Io(e) => e.fmt(f),
91 CommitError::Cipher(e) => e.fmt(f),
92 }
93 }
94}
95
96impl std::error::Error for CommitError {}
97
98impl From<io::Error> for CommitError {
99 fn from(value: io::Error) -> Self {
100 CommitError::Io(value)
101 }
102}
103
104impl From<HaysonCipherError> for CommitError {
105 fn from(value: HaysonCipherError) -> Self {
106 CommitError::Cipher(value)
107 }
108}
109
110impl From<HaysonValidationError> for StageError {
111 fn from(value: HaysonValidationError) -> Self {
112 StageError::Validation(value)
113 }
114}
115
116impl From<HaysonUniqueViolationError> for StageError {
117 fn from(value: HaysonUniqueViolationError) -> Self {
118 StageError::Unique(value)
119 }
120}
121
122impl From<HaysonPartitionError> for StageError {
123 fn from(value: HaysonPartitionError) -> Self {
124 StageError::Partition(value)
125 }
126}
127
128impl From<HaysonPendingConflictError> for StageError {
129 fn from(value: HaysonPendingConflictError) -> Self {
130 StageError::Conflict(value)
131 }
132}
133
134#[derive(Debug, Clone, PartialEq)]
135pub enum WriteOutcome {
136 Put(HaysonRecord),
137 Deleted(bool),
138}
139
140pub type Seal<'a> = dyn Fn(&str, &HaysonRecord, &[String]) -> Result<HaysonRecord, HaysonCipherError> + 'a;
144
145enum EntryKind {
146 Put(HaysonRecord),
147 Delete,
148}
149
150struct QueuedEntry {
151 key: String,
152 fields: Vec<String>,
153 kind: EntryKind,
154 sealed: Vec<String>,
159 part: String,
161}
162
163pub struct WriteQueue {
167 table: String,
168 max_batch: usize,
169 pending: PendingMap,
170 queued: VecDeque<QueuedEntry>,
171 abandoned: Vec<String>,
174 image_key: Option<[u8; 32]>,
176}
177
178impl WriteQueue {
179 pub fn new(table: impl Into<String>) -> Self {
180 Self::with_max_batch(table, 256)
181 }
182
183 pub fn set_image_key(&mut self, key: Option<[u8; 32]>) {
186 self.image_key = key;
187 }
188
189 pub fn with_max_batch(table: impl Into<String>, max_batch: usize) -> Self {
190 WriteQueue {
191 table: table.into(),
192 max_batch: max_batch.max(1),
193 pending: PendingMap::new(),
194 queued: VecDeque::new(),
195 abandoned: Vec::new(),
196 image_key: None,
197 }
198 }
199
200 pub fn depth(&self) -> usize {
201 self.queued.len()
202 }
203
204 pub fn pending(&self, key: &str) -> HashSet<String> {
205 self.pending.pending(key)
206 }
207
208 pub fn take_abandoned_keys(&mut self) -> Vec<String> {
211 std::mem::take(&mut self.abandoned)
212 }
213
214 pub fn queued_keys(&self) -> impl Iterator<Item = &str> {
218 self.queued.iter().map(|entry| entry.key.as_str())
219 }
220
221 pub fn stage_put(
225 &mut self,
226 schema: &TableSchema,
227 registry: &TableRegistry,
228 document: &Value,
229 force: bool,
230 ) -> Result<String, StageError> {
231 self.stage_entry(schema, registry, document, None, Vec::new(), force)
232 }
233
234 pub fn stage_put_carrying(
239 &mut self,
240 schema: &TableSchema,
241 registry: &TableRegistry,
242 document: &Value,
243 sealed: Vec<String>,
244 force: bool,
245 ) -> Result<String, StageError> {
246 self.stage_entry(schema, registry, document, None, sealed, force)
247 }
248
249 pub fn stage_put_claiming(
255 &mut self,
256 schema: &TableSchema,
257 registry: &TableRegistry,
258 document: &Value,
259 claim: &[String],
260 force: bool,
261 ) -> Result<String, StageError> {
262 self.stage_entry(schema, registry, document, Some(claim), Vec::new(), force)
263 }
264
265 fn stage_entry(
266 &mut self,
267 schema: &TableSchema,
268 registry: &TableRegistry,
269 document: &Value,
270 claim: Option<&[String]>,
271 sealed: Vec<String>,
272 force: bool,
273 ) -> Result<String, StageError> {
274 let record = validate_record(
275 schema,
276 document,
277 ValidateOptions {
278 image_key: self.image_key,
279 ..ValidateOptions::default()
280 },
281 )?;
282 let key = primary_key_of(schema, &record)?;
283 assert_unique(schema, registry, &key, &record)?;
284 let part = partition_of(schema, &record)?;
285 assert_partition_stable(schema, registry, &key, &part)?;
286 let fields: Vec<String> = match claim {
287 Some(claim) => claim.to_vec(),
288 None => record.keys().cloned().collect(),
289 };
290 self.reserve(&key, &fields, force)?;
291 self.queued.push_back(QueuedEntry {
292 key: key.clone(),
293 fields,
294 kind: EntryKind::Put(record),
295 sealed,
296 part,
297 });
298 Ok(key)
299 }
300
301 pub fn last_put_record(&self) -> Option<&HaysonRecord> {
305 match self.queued.back() {
306 Some(QueuedEntry {
307 kind: EntryKind::Put(record),
308 ..
309 }) => Some(record),
310 _ => None,
311 }
312 }
313
314 pub fn stage_delete(
318 &mut self,
319 key: impl Into<String>,
320 force: bool,
321 part: &str,
322 ) -> Result<(), HaysonPendingConflictError> {
323 let key = key.into();
324 let fields = vec![ALL_FIELDS.to_string()];
325 self.reserve(&key, &fields, force)?;
326 self.queued.push_back(QueuedEntry {
327 key,
328 fields,
329 kind: EntryKind::Delete,
330 sealed: Vec::new(),
331 part: part.to_string(),
332 });
333 Ok(())
334 }
335
336 fn reserve(&mut self, key: &str, fields: &[String], force: bool) -> Result<(), HaysonPendingConflictError> {
337 let conflicts = self.pending.conflicts_of(key, fields);
338 if !conflicts.is_empty() && !force {
339 return Err(HaysonPendingConflictError {
340 table: self.table.clone(),
341 key: key.to_string(),
342 fields: conflicts,
343 });
344 }
345 self.pending.claim(key, fields);
346 Ok(())
347 }
348
349 #[allow(dead_code)]
355 pub fn commit<L: LogAppender>(
356 &mut self,
357 log: &mut L,
358 registry: &mut TableRegistry,
359 seq: &mut u64,
360 sync: bool,
361 ) -> io::Result<Vec<WriteOutcome>> {
362 self.commit_sealed(log, registry, seq, sync, &|_key, record, _sealed| Ok(record.clone()))
363 .map_err(|error| match error {
364 CommitError::Io(error) => error,
365 CommitError::Cipher(error) => io::Error::other(error.to_string()),
366 })
367 }
368
369 pub fn commit_sealed<L: LogAppender>(
383 &mut self,
384 log: &mut L,
385 registry: &mut TableRegistry,
386 seq: &mut u64,
387 sync: bool,
388 seal: &Seal<'_>,
389 ) -> Result<Vec<WriteOutcome>, CommitError> {
390 let mut results = Vec::with_capacity(self.queued.len());
391 while !self.queued.is_empty() {
392 let take = self.max_batch.min(self.queued.len());
393 let now = chrono::Utc::now().timestamp_millis();
394
395 let landed = self.stamp_batch(take, seq, now, seal).and_then(|(lines, stamps)| {
396 let batch = &self.queued.make_contiguous()[..take];
397 let slots = append_by_part(log, batch, &lines)?;
398 if sync {
399 log.sync()?;
400 }
401 Ok((slots, stamps))
402 });
403 let (slots, stamps) = match landed {
404 Ok(landed) => landed,
405 Err(error) => {
406 self.abandon_queued();
407 return Err(error);
408 }
409 };
410
411 for ((entry, slot), commit_seq) in self.queued.drain(..take).zip(slots).zip(stamps) {
412 match entry.kind {
413 EntryKind::Put(record) => {
414 registry.apply_put_in(&entry.key, &record, slot, commit_seq, &entry.part);
415 results.push(WriteOutcome::Put(record));
416 }
417 EntryKind::Delete => {
418 let existed = registry.slot_of(&entry.key).is_some();
419 registry.apply_delete_in(&entry.key, slot.length, commit_seq, &entry.part);
420 results.push(WriteOutcome::Deleted(existed));
421 }
422 }
423 self.pending.release(&entry.key, &entry.fields);
424 }
425 }
426 Ok(results)
427 }
428
429 fn stamp_batch(
432 &self,
433 take: usize,
434 seq: &mut u64,
435 now: i64,
436 seal: &Seal<'_>,
437 ) -> Result<(Vec<String>, Vec<u64>), CommitError> {
438 let mut lines = Vec::with_capacity(take);
439 let mut stamps = Vec::with_capacity(take);
440 for entry in self.queued.iter().take(take) {
441 *seq += 1;
442 stamps.push(*seq);
443 let line = match &entry.kind {
444 EntryKind::Put(record) => {
445 let sealed = seal(&entry.key, record, &entry.sealed)?;
446 serialize_entry(
447 &LogMeta {
448 key: entry.key.clone(),
449 op: LogOp::Put,
450 seq: *seq,
451 ts: now,
452 },
453 Some(&sealed),
454 )
455 }
456 EntryKind::Delete => serialize_entry(
457 &LogMeta {
458 key: entry.key.clone(),
459 op: LogOp::Del,
460 seq: *seq,
461 ts: now,
462 },
463 None,
464 ),
465 };
466 lines.push(line);
467 }
468 Ok((lines, stamps))
469 }
470
471 fn abandon_queued(&mut self) {
476 for entry in self.queued.drain(..) {
477 self.pending.release(&entry.key, &entry.fields);
478 self.abandoned.push(entry.key);
479 }
480 }
481}
482
483fn append_by_part<L: LogAppender + ?Sized>(
488 log: &mut L,
489 batch: &[QueuedEntry],
490 lines: &[String],
491) -> io::Result<Vec<Slot>> {
492 let mut grouped: Vec<(&str, Vec<usize>)> = Vec::new();
493 for (position, entry) in batch.iter().enumerate() {
494 match grouped.iter_mut().find(|(part, _)| *part == entry.part) {
495 Some((_, positions)) => positions.push(position),
496 None => grouped.push((&entry.part, vec![position])),
497 }
498 }
499
500 let mut slots: Vec<Option<Slot>> = vec![None; batch.len()];
501 for (part, positions) in grouped {
502 let payload: Vec<String> = positions.iter().map(|position| lines[*position].clone()).collect();
503 let appended = log.append_lines_in(part, &payload)?;
504 for (position, slot) in positions.into_iter().zip(appended) {
505 slots[position] = Some(slot);
506 }
507 }
508 slots
509 .into_iter()
510 .map(|slot| slot.ok_or_else(|| io::Error::other("the log returned fewer slots than lines appended")))
511 .collect()
512}
513
514pub(crate) fn assert_partition_stable(
518 schema: &TableSchema,
519 registry: &TableRegistry,
520 key: &str,
521 part: &str,
522) -> Result<(), HaysonPartitionError> {
523 if part == UNPARTITIONED {
524 return Ok(());
525 }
526 let Some(held) = registry.part_of(key) else {
527 return Ok(());
528 };
529 if held == part {
530 return Ok(());
531 }
532 Err(HaysonPartitionError::new(
533 schema.table.clone(),
534 schema.partition_by.clone().unwrap_or_default(),
535 format!("key \"{key}\" already lives in partition \"{held}\" — delete it before writing it into \"{part}\""),
536 ))
537}
538
539pub(crate) type UnitShadow = HashMap<String, Option<HaysonRecord>>;
543
544pub(crate) fn assert_unique(
545 schema: &TableSchema,
546 registry: &TableRegistry,
547 key: &str,
548 record: &HaysonRecord,
549) -> Result<(), HaysonUniqueViolationError> {
550 assert_unique_in_unit(schema, registry, key, record, &UnitShadow::new())
551}
552
553fn virtual_path_of<'a>(record: &'a HaysonRecord, field: &str) -> Option<&'a str> {
554 record.get(field)?.as_object()?.get("virtualPath")?.as_str()
555}
556
557fn violation(schema: &TableSchema, field: String, holder: String) -> HaysonUniqueViolationError {
558 HaysonUniqueViolationError {
559 table: schema.table.clone(),
560 field,
561 holder,
562 }
563}
564
565fn shadow_released(
569 shadow: &UnitShadow,
570 holder: &str,
571 mine: &str,
572 of: impl Fn(&HaysonRecord) -> Option<String>,
573) -> bool {
574 match shadow.get(holder) {
575 None => false,
576 Some(None) => true,
577 Some(Some(record)) => of(record).as_deref() != Some(mine),
578 }
579}
580
581pub(crate) fn assert_unique_in_unit(
586 schema: &TableSchema,
587 registry: &TableRegistry,
588 key: &str,
589 record: &HaysonRecord,
590 shadow: &UnitShadow,
591) -> Result<(), HaysonUniqueViolationError> {
592 for index in registry.indexes() {
593 if !index.unique {
594 continue;
595 }
596 if index.fields.len() == 1 && index.fields[0] == schema.primary_key {
597 continue;
598 }
599 let unique_key = |candidate: &HaysonRecord| unique_key_of(candidate, &index.fields);
600 let Some(mine) = unique_key(record) else { continue };
601 for (other_key, other) in shadow {
602 if other_key == key {
603 continue;
604 }
605 let Some(other) = other else { continue };
606 if unique_key(other).as_deref() == Some(mine.as_str()) {
607 return Err(violation(schema, index.fields.join(", "), other_key.clone()));
608 }
609 }
610 if let Some(holder) = registry.unique_holder_of(index, record) {
611 if holder != key && !shadow_released(shadow, &holder, &mine, unique_key) {
612 return Err(violation(schema, index.fields.join(", "), holder));
613 }
614 }
615 }
616 for field in registry.virtual_path_fields() {
617 let path_of = |candidate: &HaysonRecord| virtual_path_of(candidate, field).map(str::to_string);
618 let Some(mine) = virtual_path_of(record, field) else {
619 continue;
620 };
621 for (other_key, other) in shadow {
622 if other_key == key {
623 continue;
624 }
625 let Some(other) = other else { continue };
626 if virtual_path_of(other, field) == Some(mine) {
627 return Err(violation(schema, field.clone(), other_key.clone()));
628 }
629 }
630 if let Some(holder) = registry.virtual_path_holder(field, mine) {
631 if holder != key && !shadow_released(shadow, &holder, mine, path_of) {
632 return Err(violation(schema, field.clone(), holder));
633 }
634 }
635 }
636 Ok(())
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642 use crate::schema::parse_schema;
643 use serde_json::json;
644
645 fn users_schema() -> TableSchema {
646 parse_schema(&json!({
647 "schemaVersion": 1,
648 "table": "users",
649 "primaryKey": "id",
650 "fields": {
651 "id": { "type": "string" },
652 "name": { "type": "string", "required": true },
653 "email": { "type": "string", "unique": true },
654 },
655 }))
656 .unwrap()
657 }
658
659 struct CountingLog {
660 calls: usize,
661 synced: usize,
662 appended: Vec<String>,
663 }
664
665 impl CountingLog {
666 fn new() -> Self {
667 CountingLog {
668 calls: 0,
669 synced: 0,
670 appended: Vec::new(),
671 }
672 }
673 }
674
675 impl LogAppender for CountingLog {
676 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
677 self.calls += 1;
678 let start = self.appended.len() as u64;
679 let slots = lines
680 .iter()
681 .enumerate()
682 .map(|(i, line)| Slot {
683 offset: start + i as u64,
684 length: line.len() as u64,
685 })
686 .collect();
687 self.appended.extend(lines.iter().cloned());
688 Ok(slots)
689 }
690
691 fn sync(&mut self) -> io::Result<()> {
692 self.synced += 1;
693 Ok(())
694 }
695 }
696
697 #[test]
698 fn commits_every_entry_queued_since_the_last_call_in_one_append() {
699 let schema = users_schema();
700 let mut registry = TableRegistry::new(schema.clone());
701 let mut queue = WriteQueue::new("users");
702 let mut log = CountingLog::new();
703 let mut seq = 0u64;
704
705 queue
706 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
707 .unwrap();
708 queue
709 .stage_put(&schema, ®istry, &json!({ "id": "u2", "name": "Piotr" }), false)
710 .unwrap();
711 assert_eq!(queue.depth(), 2);
712
713 let results = queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
714
715 assert_eq!(log.calls, 1);
716 assert_eq!(log.synced, 1);
717 assert_eq!(results.len(), 2);
718 assert_eq!(queue.depth(), 0);
719 assert_eq!(registry.records(), 2);
720 }
721
722 #[test]
723 fn a_second_write_to_the_same_field_conflicts_unless_forced() {
724 let schema = users_schema();
725 let registry = TableRegistry::new(schema.clone());
726 let mut queue = WriteQueue::new("users");
727
728 queue
729 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
730 .unwrap();
731 let err = queue
732 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), false)
733 .unwrap_err();
734 assert!(matches!(err, StageError::Conflict(_)));
735
736 queue
737 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), true)
738 .unwrap();
739 assert_eq!(queue.depth(), 2);
740 }
741
742 #[test]
743 fn a_delete_claims_every_field_and_conflicts_with_a_write_on_any_of_them() {
744 let schema = users_schema();
745 let registry = TableRegistry::new(schema.clone());
746 let mut queue = WriteQueue::new("users");
747
748 queue.stage_delete("u1", false, "").unwrap();
749 let err = queue
750 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
751 .unwrap_err();
752 assert!(matches!(err, StageError::Conflict(_)));
753 }
754
755 #[test]
756 fn releases_the_claim_only_after_the_batch_commits() {
757 let schema = users_schema();
758 let mut registry = TableRegistry::new(schema.clone());
759 let mut queue = WriteQueue::new("users");
760 let mut log = CountingLog::new();
761 let mut seq = 0u64;
762
763 queue
764 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
765 .unwrap();
766 assert_eq!(queue.pending("u1"), HashSet::from(["id".into(), "name".into()]));
767
768 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
769 assert!(queue.pending("u1").is_empty());
770 }
771
772 struct RefusingLog {
775 inner: CountingLog,
776 failing_from: usize,
777 }
778
779 impl LogAppender for RefusingLog {
780 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
781 if self.inner.calls + 1 >= self.failing_from {
782 self.inner.calls += 1;
783 return Err(io::Error::other("refused"));
784 }
785 self.inner.append_lines(lines)
786 }
787
788 fn sync(&mut self) -> io::Result<()> {
789 self.inner.sync()
790 }
791 }
792
793 #[test]
794 fn a_refused_append_leaves_nothing_queued_and_nothing_claimed() {
795 let schema = users_schema();
796 let mut registry = TableRegistry::new(schema.clone());
797 let mut queue = WriteQueue::new("users");
798 let mut log = RefusingLog {
799 inner: CountingLog::new(),
800 failing_from: 1,
801 };
802 let mut seq = 0u64;
803 queue
804 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
805 .unwrap();
806
807 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap_err();
808
809 assert_eq!(queue.depth(), 0, "the refused entry stayed queued");
810 assert!(queue.pending("u1").is_empty(), "the refused entry kept its claim");
811 assert_eq!(queue.take_abandoned_keys(), vec!["u1".to_string()]);
812 assert!(queue.take_abandoned_keys().is_empty(), "the keys are handed over once");
813 assert_eq!(registry.records(), 0);
814 queue
815 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
816 .expect("the same write can be staged again without force");
817 }
818
819 #[test]
820 fn a_refusal_mid_way_keeps_the_batches_that_landed_and_abandons_the_rest() {
821 let schema = users_schema();
822 let mut registry = TableRegistry::new(schema.clone());
823 let mut queue = WriteQueue::with_max_batch("users", 1);
824 let mut log = RefusingLog {
825 inner: CountingLog::new(),
826 failing_from: 2,
827 };
828 let mut seq = 0u64;
829 for (id, name) in [("u1", "Ania"), ("u2", "Piotr"), ("u3", "Zosia")] {
830 queue
831 .stage_put(&schema, ®istry, &json!({ "id": id, "name": name }), false)
832 .unwrap();
833 }
834
835 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap_err();
836
837 assert_eq!(
838 registry.records(),
839 1,
840 "the batch that landed before the refusal is committed"
841 );
842 assert_eq!(queue.depth(), 0);
843 assert_eq!(queue.take_abandoned_keys(), vec!["u2".to_string(), "u3".to_string()]);
844 for key in ["u1", "u2", "u3"] {
845 assert!(queue.pending(key).is_empty(), "{key} kept its claim");
846 }
847 }
848
849 #[test]
850 fn rejects_a_second_record_claiming_a_unique_field_already_held() {
851 let schema = users_schema();
852 let mut registry = TableRegistry::new(schema.clone());
853 let mut queue = WriteQueue::new("users");
854 let mut log = CountingLog::new();
855 let mut seq = 0u64;
856
857 queue
858 .stage_put(
859 &schema,
860 ®istry,
861 &json!({ "id": "u1", "name": "Ania", "email": "a@x.io" }),
862 false,
863 )
864 .unwrap();
865 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
866
867 let err = queue
868 .stage_put(
869 &schema,
870 ®istry,
871 &json!({ "id": "u2", "name": "Zosia", "email": "a@x.io" }),
872 false,
873 )
874 .unwrap_err();
875 assert!(matches!(err, StageError::Unique(_)));
876 }
877
878 fn docs_schema() -> TableSchema {
879 parse_schema(&json!({
880 "schemaVersion": 1,
881 "table": "docs",
882 "primaryKey": "id",
883 "fields": {
884 "id": { "type": "string" },
885 "page": { "type": "markdown" },
886 },
887 }))
888 .unwrap()
889 }
890
891 #[test]
892 fn rejects_a_second_record_claiming_an_already_taken_markdown_virtual_path() {
893 let schema = docs_schema();
894 let mut registry = TableRegistry::new(schema.clone());
895 let mut queue = WriteQueue::new("docs");
896 let mut log = CountingLog::new();
897 let mut seq = 0u64;
898
899 queue
900 .stage_put(
901 &schema,
902 ®istry,
903 &json!({ "id": "d1", "page": { "content": "a", "virtualPath": "/docs/a.md" } }),
904 false,
905 )
906 .unwrap();
907 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
908
909 let err = queue
910 .stage_put(
911 &schema,
912 ®istry,
913 &json!({ "id": "d2", "page": { "content": "b", "virtualPath": "/docs/a.md" } }),
914 false,
915 )
916 .unwrap_err();
917 assert!(matches!(err, StageError::Unique(_)));
918 }
919
920 #[test]
921 fn dead_bytes_grow_when_an_existing_key_is_overwritten_or_removed() {
922 let schema = users_schema();
923 let mut registry = TableRegistry::new(schema.clone());
924 let mut queue = WriteQueue::new("users");
925 let mut log = CountingLog::new();
926 let mut seq = 0u64;
927
928 queue
929 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
930 .unwrap();
931 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
932 assert_eq!(registry.stats().dead_bytes, 0);
933
934 queue
935 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), false)
936 .unwrap();
937 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
938 let after_overwrite = registry.stats().dead_bytes;
939 assert!(after_overwrite > 0);
940
941 queue.stage_delete("u1", false, "").unwrap();
942 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
943 assert!(registry.stats().dead_bytes > after_overwrite);
944 assert_eq!(registry.records(), 0);
945 }
946
947 #[test]
948 fn splits_a_batch_larger_than_max_batch_into_several_appends() {
949 let schema = users_schema();
950 let mut registry = TableRegistry::new(schema.clone());
951 let mut queue = WriteQueue::with_max_batch("users", 2);
952 let mut log = CountingLog::new();
953 let mut seq = 0u64;
954
955 for i in 0..5 {
956 queue
957 .stage_put(
958 &schema,
959 ®istry,
960 &json!({ "id": format!("u{i}"), "name": "x" }),
961 false,
962 )
963 .unwrap();
964 }
965 let results = queue.commit(&mut log, &mut registry, &mut seq, false).unwrap();
966 assert_eq!(results.len(), 5);
967 assert_eq!(log.calls, 3);
968 assert_eq!(log.synced, 0);
969 }
970}