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}
175
176impl WriteQueue {
177 pub fn new(table: impl Into<String>) -> Self {
178 Self::with_max_batch(table, 256)
179 }
180
181 pub fn with_max_batch(table: impl Into<String>, max_batch: usize) -> Self {
182 WriteQueue {
183 table: table.into(),
184 max_batch: max_batch.max(1),
185 pending: PendingMap::new(),
186 queued: VecDeque::new(),
187 abandoned: Vec::new(),
188 }
189 }
190
191 pub fn depth(&self) -> usize {
192 self.queued.len()
193 }
194
195 pub fn pending(&self, key: &str) -> HashSet<String> {
196 self.pending.pending(key)
197 }
198
199 pub fn take_abandoned_keys(&mut self) -> Vec<String> {
202 std::mem::take(&mut self.abandoned)
203 }
204
205 pub fn queued_keys(&self) -> impl Iterator<Item = &str> {
209 self.queued.iter().map(|entry| entry.key.as_str())
210 }
211
212 pub fn stage_put(
216 &mut self,
217 schema: &TableSchema,
218 registry: &TableRegistry,
219 document: &Value,
220 force: bool,
221 ) -> Result<String, StageError> {
222 self.stage_entry(schema, registry, document, None, Vec::new(), force)
223 }
224
225 pub fn stage_put_carrying(
230 &mut self,
231 schema: &TableSchema,
232 registry: &TableRegistry,
233 document: &Value,
234 sealed: Vec<String>,
235 force: bool,
236 ) -> Result<String, StageError> {
237 self.stage_entry(schema, registry, document, None, sealed, force)
238 }
239
240 pub fn stage_put_claiming(
246 &mut self,
247 schema: &TableSchema,
248 registry: &TableRegistry,
249 document: &Value,
250 claim: &[String],
251 force: bool,
252 ) -> Result<String, StageError> {
253 self.stage_entry(schema, registry, document, Some(claim), Vec::new(), force)
254 }
255
256 fn stage_entry(
257 &mut self,
258 schema: &TableSchema,
259 registry: &TableRegistry,
260 document: &Value,
261 claim: Option<&[String]>,
262 sealed: Vec<String>,
263 force: bool,
264 ) -> Result<String, StageError> {
265 let record = validate_record(schema, document, ValidateOptions::default())?;
266 let key = primary_key_of(schema, &record)?;
267 assert_unique(schema, registry, &key, &record)?;
268 let part = partition_of(schema, &record)?;
269 assert_partition_stable(schema, registry, &key, &part)?;
270 let fields: Vec<String> = match claim {
271 Some(claim) => claim.to_vec(),
272 None => record.keys().cloned().collect(),
273 };
274 self.reserve(&key, &fields, force)?;
275 self.queued.push_back(QueuedEntry {
276 key: key.clone(),
277 fields,
278 kind: EntryKind::Put(record),
279 sealed,
280 part,
281 });
282 Ok(key)
283 }
284
285 pub fn last_put_record(&self) -> Option<&HaysonRecord> {
289 match self.queued.back() {
290 Some(QueuedEntry {
291 kind: EntryKind::Put(record),
292 ..
293 }) => Some(record),
294 _ => None,
295 }
296 }
297
298 pub fn stage_delete(
302 &mut self,
303 key: impl Into<String>,
304 force: bool,
305 part: &str,
306 ) -> Result<(), HaysonPendingConflictError> {
307 let key = key.into();
308 let fields = vec![ALL_FIELDS.to_string()];
309 self.reserve(&key, &fields, force)?;
310 self.queued.push_back(QueuedEntry {
311 key,
312 fields,
313 kind: EntryKind::Delete,
314 sealed: Vec::new(),
315 part: part.to_string(),
316 });
317 Ok(())
318 }
319
320 fn reserve(&mut self, key: &str, fields: &[String], force: bool) -> Result<(), HaysonPendingConflictError> {
321 let conflicts = self.pending.conflicts_of(key, fields);
322 if !conflicts.is_empty() && !force {
323 return Err(HaysonPendingConflictError {
324 table: self.table.clone(),
325 key: key.to_string(),
326 fields: conflicts,
327 });
328 }
329 self.pending.claim(key, fields);
330 Ok(())
331 }
332
333 #[allow(dead_code)]
339 pub fn commit<L: LogAppender>(
340 &mut self,
341 log: &mut L,
342 registry: &mut TableRegistry,
343 seq: &mut u64,
344 sync: bool,
345 ) -> io::Result<Vec<WriteOutcome>> {
346 self.commit_sealed(log, registry, seq, sync, &|_key, record, _sealed| Ok(record.clone()))
347 .map_err(|error| match error {
348 CommitError::Io(error) => error,
349 CommitError::Cipher(error) => io::Error::other(error.to_string()),
350 })
351 }
352
353 pub fn commit_sealed<L: LogAppender>(
367 &mut self,
368 log: &mut L,
369 registry: &mut TableRegistry,
370 seq: &mut u64,
371 sync: bool,
372 seal: &Seal<'_>,
373 ) -> Result<Vec<WriteOutcome>, CommitError> {
374 let mut results = Vec::with_capacity(self.queued.len());
375 while !self.queued.is_empty() {
376 let take = self.max_batch.min(self.queued.len());
377 let now = chrono::Utc::now().timestamp_millis();
378
379 let landed = self.stamp_batch(take, seq, now, seal).and_then(|(lines, stamps)| {
380 let batch = &self.queued.make_contiguous()[..take];
381 let slots = append_by_part(log, batch, &lines)?;
382 if sync {
383 log.sync()?;
384 }
385 Ok((slots, stamps))
386 });
387 let (slots, stamps) = match landed {
388 Ok(landed) => landed,
389 Err(error) => {
390 self.abandon_queued();
391 return Err(error);
392 }
393 };
394
395 for ((entry, slot), commit_seq) in self.queued.drain(..take).zip(slots).zip(stamps) {
396 match entry.kind {
397 EntryKind::Put(record) => {
398 registry.apply_put_in(&entry.key, &record, slot, commit_seq, &entry.part);
399 results.push(WriteOutcome::Put(record));
400 }
401 EntryKind::Delete => {
402 let existed = registry.slot_of(&entry.key).is_some();
403 registry.apply_delete_in(&entry.key, slot.length, commit_seq, &entry.part);
404 results.push(WriteOutcome::Deleted(existed));
405 }
406 }
407 self.pending.release(&entry.key, &entry.fields);
408 }
409 }
410 Ok(results)
411 }
412
413 fn stamp_batch(
416 &self,
417 take: usize,
418 seq: &mut u64,
419 now: i64,
420 seal: &Seal<'_>,
421 ) -> Result<(Vec<String>, Vec<u64>), CommitError> {
422 let mut lines = Vec::with_capacity(take);
423 let mut stamps = Vec::with_capacity(take);
424 for entry in self.queued.iter().take(take) {
425 *seq += 1;
426 stamps.push(*seq);
427 let line = match &entry.kind {
428 EntryKind::Put(record) => {
429 let sealed = seal(&entry.key, record, &entry.sealed)?;
430 serialize_entry(
431 &LogMeta {
432 key: entry.key.clone(),
433 op: LogOp::Put,
434 seq: *seq,
435 ts: now,
436 },
437 Some(&sealed),
438 )
439 }
440 EntryKind::Delete => serialize_entry(
441 &LogMeta {
442 key: entry.key.clone(),
443 op: LogOp::Del,
444 seq: *seq,
445 ts: now,
446 },
447 None,
448 ),
449 };
450 lines.push(line);
451 }
452 Ok((lines, stamps))
453 }
454
455 fn abandon_queued(&mut self) {
460 for entry in self.queued.drain(..) {
461 self.pending.release(&entry.key, &entry.fields);
462 self.abandoned.push(entry.key);
463 }
464 }
465}
466
467fn append_by_part<L: LogAppender + ?Sized>(
472 log: &mut L,
473 batch: &[QueuedEntry],
474 lines: &[String],
475) -> io::Result<Vec<Slot>> {
476 let mut grouped: Vec<(&str, Vec<usize>)> = Vec::new();
477 for (position, entry) in batch.iter().enumerate() {
478 match grouped.iter_mut().find(|(part, _)| *part == entry.part) {
479 Some((_, positions)) => positions.push(position),
480 None => grouped.push((&entry.part, vec![position])),
481 }
482 }
483
484 let mut slots: Vec<Option<Slot>> = vec![None; batch.len()];
485 for (part, positions) in grouped {
486 let payload: Vec<String> = positions.iter().map(|position| lines[*position].clone()).collect();
487 let appended = log.append_lines_in(part, &payload)?;
488 for (position, slot) in positions.into_iter().zip(appended) {
489 slots[position] = Some(slot);
490 }
491 }
492 slots
493 .into_iter()
494 .map(|slot| slot.ok_or_else(|| io::Error::other("the log returned fewer slots than lines appended")))
495 .collect()
496}
497
498pub(crate) fn assert_partition_stable(
502 schema: &TableSchema,
503 registry: &TableRegistry,
504 key: &str,
505 part: &str,
506) -> Result<(), HaysonPartitionError> {
507 if part == UNPARTITIONED {
508 return Ok(());
509 }
510 let Some(held) = registry.part_of(key) else {
511 return Ok(());
512 };
513 if held == part {
514 return Ok(());
515 }
516 Err(HaysonPartitionError::new(
517 schema.table.clone(),
518 schema.partition_by.clone().unwrap_or_default(),
519 format!("key \"{key}\" already lives in partition \"{held}\" — delete it before writing it into \"{part}\""),
520 ))
521}
522
523pub(crate) type UnitShadow = HashMap<String, Option<HaysonRecord>>;
527
528pub(crate) fn assert_unique(
529 schema: &TableSchema,
530 registry: &TableRegistry,
531 key: &str,
532 record: &HaysonRecord,
533) -> Result<(), HaysonUniqueViolationError> {
534 assert_unique_in_unit(schema, registry, key, record, &UnitShadow::new())
535}
536
537fn virtual_path_of<'a>(record: &'a HaysonRecord, field: &str) -> Option<&'a str> {
538 record.get(field)?.as_object()?.get("virtualPath")?.as_str()
539}
540
541fn violation(schema: &TableSchema, field: String, holder: String) -> HaysonUniqueViolationError {
542 HaysonUniqueViolationError {
543 table: schema.table.clone(),
544 field,
545 holder,
546 }
547}
548
549fn shadow_released(
553 shadow: &UnitShadow,
554 holder: &str,
555 mine: &str,
556 of: impl Fn(&HaysonRecord) -> Option<String>,
557) -> bool {
558 match shadow.get(holder) {
559 None => false,
560 Some(None) => true,
561 Some(Some(record)) => of(record).as_deref() != Some(mine),
562 }
563}
564
565pub(crate) fn assert_unique_in_unit(
570 schema: &TableSchema,
571 registry: &TableRegistry,
572 key: &str,
573 record: &HaysonRecord,
574 shadow: &UnitShadow,
575) -> Result<(), HaysonUniqueViolationError> {
576 for index in registry.indexes() {
577 if !index.unique {
578 continue;
579 }
580 if index.fields.len() == 1 && index.fields[0] == schema.primary_key {
581 continue;
582 }
583 let unique_key = |candidate: &HaysonRecord| unique_key_of(candidate, &index.fields);
584 let Some(mine) = unique_key(record) else { continue };
585 for (other_key, other) in shadow {
586 if other_key == key {
587 continue;
588 }
589 let Some(other) = other else { continue };
590 if unique_key(other).as_deref() == Some(mine.as_str()) {
591 return Err(violation(schema, index.fields.join(", "), other_key.clone()));
592 }
593 }
594 if let Some(holder) = registry.unique_holder_of(index, record) {
595 if holder != key && !shadow_released(shadow, &holder, &mine, unique_key) {
596 return Err(violation(schema, index.fields.join(", "), holder));
597 }
598 }
599 }
600 for field in registry.virtual_path_fields() {
601 let path_of = |candidate: &HaysonRecord| virtual_path_of(candidate, field).map(str::to_string);
602 let Some(mine) = virtual_path_of(record, field) else {
603 continue;
604 };
605 for (other_key, other) in shadow {
606 if other_key == key {
607 continue;
608 }
609 let Some(other) = other else { continue };
610 if virtual_path_of(other, field) == Some(mine) {
611 return Err(violation(schema, field.clone(), other_key.clone()));
612 }
613 }
614 if let Some(holder) = registry.virtual_path_holder(field, mine) {
615 if holder != key && !shadow_released(shadow, &holder, mine, path_of) {
616 return Err(violation(schema, field.clone(), holder));
617 }
618 }
619 }
620 Ok(())
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::schema::parse_schema;
627 use serde_json::json;
628
629 fn users_schema() -> TableSchema {
630 parse_schema(&json!({
631 "schemaVersion": 1,
632 "table": "users",
633 "primaryKey": "id",
634 "fields": {
635 "id": { "type": "string" },
636 "name": { "type": "string", "required": true },
637 "email": { "type": "string", "unique": true },
638 },
639 }))
640 .unwrap()
641 }
642
643 struct CountingLog {
644 calls: usize,
645 synced: usize,
646 appended: Vec<String>,
647 }
648
649 impl CountingLog {
650 fn new() -> Self {
651 CountingLog {
652 calls: 0,
653 synced: 0,
654 appended: Vec::new(),
655 }
656 }
657 }
658
659 impl LogAppender for CountingLog {
660 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
661 self.calls += 1;
662 let start = self.appended.len() as u64;
663 let slots = lines
664 .iter()
665 .enumerate()
666 .map(|(i, line)| Slot {
667 offset: start + i as u64,
668 length: line.len() as u64,
669 })
670 .collect();
671 self.appended.extend(lines.iter().cloned());
672 Ok(slots)
673 }
674
675 fn sync(&mut self) -> io::Result<()> {
676 self.synced += 1;
677 Ok(())
678 }
679 }
680
681 #[test]
682 fn commits_every_entry_queued_since_the_last_call_in_one_append() {
683 let schema = users_schema();
684 let mut registry = TableRegistry::new(schema.clone());
685 let mut queue = WriteQueue::new("users");
686 let mut log = CountingLog::new();
687 let mut seq = 0u64;
688
689 queue
690 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
691 .unwrap();
692 queue
693 .stage_put(&schema, ®istry, &json!({ "id": "u2", "name": "Piotr" }), false)
694 .unwrap();
695 assert_eq!(queue.depth(), 2);
696
697 let results = queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
698
699 assert_eq!(log.calls, 1);
700 assert_eq!(log.synced, 1);
701 assert_eq!(results.len(), 2);
702 assert_eq!(queue.depth(), 0);
703 assert_eq!(registry.records(), 2);
704 }
705
706 #[test]
707 fn a_second_write_to_the_same_field_conflicts_unless_forced() {
708 let schema = users_schema();
709 let registry = TableRegistry::new(schema.clone());
710 let mut queue = WriteQueue::new("users");
711
712 queue
713 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
714 .unwrap();
715 let err = queue
716 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), false)
717 .unwrap_err();
718 assert!(matches!(err, StageError::Conflict(_)));
719
720 queue
721 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), true)
722 .unwrap();
723 assert_eq!(queue.depth(), 2);
724 }
725
726 #[test]
727 fn a_delete_claims_every_field_and_conflicts_with_a_write_on_any_of_them() {
728 let schema = users_schema();
729 let registry = TableRegistry::new(schema.clone());
730 let mut queue = WriteQueue::new("users");
731
732 queue.stage_delete("u1", false, "").unwrap();
733 let err = queue
734 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
735 .unwrap_err();
736 assert!(matches!(err, StageError::Conflict(_)));
737 }
738
739 #[test]
740 fn releases_the_claim_only_after_the_batch_commits() {
741 let schema = users_schema();
742 let mut registry = TableRegistry::new(schema.clone());
743 let mut queue = WriteQueue::new("users");
744 let mut log = CountingLog::new();
745 let mut seq = 0u64;
746
747 queue
748 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
749 .unwrap();
750 assert_eq!(queue.pending("u1"), HashSet::from(["id".into(), "name".into()]));
751
752 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
753 assert!(queue.pending("u1").is_empty());
754 }
755
756 struct RefusingLog {
759 inner: CountingLog,
760 failing_from: usize,
761 }
762
763 impl LogAppender for RefusingLog {
764 fn append_lines(&mut self, lines: &[String]) -> io::Result<Vec<Slot>> {
765 if self.inner.calls + 1 >= self.failing_from {
766 self.inner.calls += 1;
767 return Err(io::Error::other("refused"));
768 }
769 self.inner.append_lines(lines)
770 }
771
772 fn sync(&mut self) -> io::Result<()> {
773 self.inner.sync()
774 }
775 }
776
777 #[test]
778 fn a_refused_append_leaves_nothing_queued_and_nothing_claimed() {
779 let schema = users_schema();
780 let mut registry = TableRegistry::new(schema.clone());
781 let mut queue = WriteQueue::new("users");
782 let mut log = RefusingLog {
783 inner: CountingLog::new(),
784 failing_from: 1,
785 };
786 let mut seq = 0u64;
787 queue
788 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
789 .unwrap();
790
791 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap_err();
792
793 assert_eq!(queue.depth(), 0, "the refused entry stayed queued");
794 assert!(queue.pending("u1").is_empty(), "the refused entry kept its claim");
795 assert_eq!(queue.take_abandoned_keys(), vec!["u1".to_string()]);
796 assert!(queue.take_abandoned_keys().is_empty(), "the keys are handed over once");
797 assert_eq!(registry.records(), 0);
798 queue
799 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
800 .expect("the same write can be staged again without force");
801 }
802
803 #[test]
804 fn a_refusal_mid_way_keeps_the_batches_that_landed_and_abandons_the_rest() {
805 let schema = users_schema();
806 let mut registry = TableRegistry::new(schema.clone());
807 let mut queue = WriteQueue::with_max_batch("users", 1);
808 let mut log = RefusingLog {
809 inner: CountingLog::new(),
810 failing_from: 2,
811 };
812 let mut seq = 0u64;
813 for (id, name) in [("u1", "Ania"), ("u2", "Piotr"), ("u3", "Zosia")] {
814 queue
815 .stage_put(&schema, ®istry, &json!({ "id": id, "name": name }), false)
816 .unwrap();
817 }
818
819 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap_err();
820
821 assert_eq!(
822 registry.records(),
823 1,
824 "the batch that landed before the refusal is committed"
825 );
826 assert_eq!(queue.depth(), 0);
827 assert_eq!(queue.take_abandoned_keys(), vec!["u2".to_string(), "u3".to_string()]);
828 for key in ["u1", "u2", "u3"] {
829 assert!(queue.pending(key).is_empty(), "{key} kept its claim");
830 }
831 }
832
833 #[test]
834 fn rejects_a_second_record_claiming_a_unique_field_already_held() {
835 let schema = users_schema();
836 let mut registry = TableRegistry::new(schema.clone());
837 let mut queue = WriteQueue::new("users");
838 let mut log = CountingLog::new();
839 let mut seq = 0u64;
840
841 queue
842 .stage_put(
843 &schema,
844 ®istry,
845 &json!({ "id": "u1", "name": "Ania", "email": "a@x.io" }),
846 false,
847 )
848 .unwrap();
849 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
850
851 let err = queue
852 .stage_put(
853 &schema,
854 ®istry,
855 &json!({ "id": "u2", "name": "Zosia", "email": "a@x.io" }),
856 false,
857 )
858 .unwrap_err();
859 assert!(matches!(err, StageError::Unique(_)));
860 }
861
862 fn docs_schema() -> TableSchema {
863 parse_schema(&json!({
864 "schemaVersion": 1,
865 "table": "docs",
866 "primaryKey": "id",
867 "fields": {
868 "id": { "type": "string" },
869 "page": { "type": "markdown" },
870 },
871 }))
872 .unwrap()
873 }
874
875 #[test]
876 fn rejects_a_second_record_claiming_an_already_taken_markdown_virtual_path() {
877 let schema = docs_schema();
878 let mut registry = TableRegistry::new(schema.clone());
879 let mut queue = WriteQueue::new("docs");
880 let mut log = CountingLog::new();
881 let mut seq = 0u64;
882
883 queue
884 .stage_put(
885 &schema,
886 ®istry,
887 &json!({ "id": "d1", "page": { "content": "a", "virtualPath": "/docs/a.md" } }),
888 false,
889 )
890 .unwrap();
891 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
892
893 let err = queue
894 .stage_put(
895 &schema,
896 ®istry,
897 &json!({ "id": "d2", "page": { "content": "b", "virtualPath": "/docs/a.md" } }),
898 false,
899 )
900 .unwrap_err();
901 assert!(matches!(err, StageError::Unique(_)));
902 }
903
904 #[test]
905 fn dead_bytes_grow_when_an_existing_key_is_overwritten_or_removed() {
906 let schema = users_schema();
907 let mut registry = TableRegistry::new(schema.clone());
908 let mut queue = WriteQueue::new("users");
909 let mut log = CountingLog::new();
910 let mut seq = 0u64;
911
912 queue
913 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Ania" }), false)
914 .unwrap();
915 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
916 assert_eq!(registry.stats().dead_bytes, 0);
917
918 queue
919 .stage_put(&schema, ®istry, &json!({ "id": "u1", "name": "Zosia" }), false)
920 .unwrap();
921 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
922 let after_overwrite = registry.stats().dead_bytes;
923 assert!(after_overwrite > 0);
924
925 queue.stage_delete("u1", false, "").unwrap();
926 queue.commit(&mut log, &mut registry, &mut seq, true).unwrap();
927 assert!(registry.stats().dead_bytes > after_overwrite);
928 assert_eq!(registry.records(), 0);
929 }
930
931 #[test]
932 fn splits_a_batch_larger_than_max_batch_into_several_appends() {
933 let schema = users_schema();
934 let mut registry = TableRegistry::new(schema.clone());
935 let mut queue = WriteQueue::with_max_batch("users", 2);
936 let mut log = CountingLog::new();
937 let mut seq = 0u64;
938
939 for i in 0..5 {
940 queue
941 .stage_put(
942 &schema,
943 ®istry,
944 &json!({ "id": format!("u{i}"), "name": "x" }),
945 false,
946 )
947 .unwrap();
948 }
949 let results = queue.commit(&mut log, &mut registry, &mut seq, false).unwrap();
950 assert_eq!(results.len(), 5);
951 assert_eq!(log.calls, 3);
952 assert_eq!(log.synced, 0);
953 }
954}