Skip to main content

hayson_engine/queue/
write_queue.rs

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    /// Appends into one named partition. Defaults to [`LogAppender::append_lines`], which is right
24    /// for anything holding a single log file: the only partition such a target has is the
25    /// [`UNPARTITIONED`] one every unpartitioned record already names.
26    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/// `WriteQueue::commit_sealed`'s failure: an I/O error appending to the log, or the `seal`
80/// closure refusing a `cypher` field (no key to write it with).
81#[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
140/// What [`WriteQueue::commit_sealed`] puts every queued `put` through on its way to a log line: the
141/// entry's key, its plaintext record, and the field names that entry was staged as already sealed
142/// with.
143pub 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    /// Field names this entry's caller declared to already hold what the `seal` closure of
155    /// [`WriteQueue::commit_sealed`] would otherwise produce. Opaque here: the queue neither reads
156    /// them nor knows what sealing is, it only keeps them tied to the entry they were staged with,
157    /// so a second write in the same window cannot inherit them.
158    sealed: Vec<String>,
159    /// The log file this entry lands in — [`UNPARTITIONED`] on a table with no `partitionBy`.
160    part: String,
161}
162
163/// Batches put/patch/delete operations across one commit cycle into a single `LogAppender` call,
164/// same shape as the TS `WriteQueue`: a key claim (`PendingMap`) guards every queued entry until the
165/// batch that carries it is committed and the registry reflects it.
166pub struct WriteQueue {
167    table: String,
168    max_batch: usize,
169    pending: PendingMap,
170    queued: VecDeque<QueuedEntry>,
171    /// The keys of every entry a refused [`WriteQueue::commit_sealed`] threw away, until the
172    /// caller collects them through [`WriteQueue::take_abandoned_keys`].
173    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    /// The keys the last refused commit abandoned — see [`WriteQueue::commit_sealed`] — handed over
200    /// once, in staging order, so the caller can drop whatever it staged beside the queue for them.
201    pub fn take_abandoned_keys(&mut self) -> Vec<String> {
202        std::mem::take(&mut self.abandoned)
203    }
204
205    /// Every key with a write still queued, in staging order — what a caller holding a cache of
206    /// committed records has to drop before [`WriteQueue::commit_sealed`] moves the registry, and
207    /// the only place that list exists before the commit drains it.
208    pub fn queued_keys(&self) -> impl Iterator<Item = &str> {
209        self.queued.iter().map(|entry| entry.key.as_str())
210    }
211
212    /// Validates `document` against `schema`, checks its unique fields against `registry`, resolves
213    /// the partition it belongs to, then reserves the record's fields in the pending map. The key
214    /// returned is the one the caller should use for `pending()`/patch chaining.
215    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    /// [`WriteQueue::stage_put`] for a record whose fields named in `sealed` must reach the log as
226    /// they stand, without the `seal` closure of [`WriteQueue::commit_sealed`] touching them. The
227    /// names ride with this entry alone — see [`QueuedEntry::sealed`] — and the queue makes no
228    /// judgement about them; whoever hands them over has already vouched for them.
229    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    /// [`WriteQueue::stage_put`] with the claim named by the caller instead of taken from the whole
241    /// record — what a patch needs, since it only changes the fields it was given and claiming the
242    /// rest would refuse a second patch touching a field this one never mentioned.
243    ///
244    /// `claim` is only what the pending map guards; the entry queued is still the whole record.
245    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    /// The record of the entry queued last — how the caller of [`WriteQueue::stage_put`] reads back
286    /// what was validated and defaulted, without paying for a second `validate_record`. `None` when
287    /// nothing is queued or the last entry queued is a delete.
288    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    /// Queues a tombstone into `part` — the partition the key currently lives in, which the caller
299    /// reads off the registry, since a tombstone has to land in the same file as the record it
300    /// retires.
301    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    /// Commits every entry queued since the last call: one `append_lines` (plus one `sync` when
334    /// requested) per batch of at most `max_batch` entries, then updates `registry` and releases
335    /// the pending claims for each committed key, in the order the entries were staged. Writes no
336    /// `cypher` field, so it fails if any queued record carries one — callers with a schema that
337    /// declares `cypher` fields must go through [`WriteQueue::commit_sealed`] instead.
338    #[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    /// Same as [`WriteQueue::commit`], but every queued `put` is passed through `seal` before it is
354    /// serialized into a log line — the plaintext record still goes to `registry` and the returned
355    /// [`WriteOutcome`], only the line written to `log` carries whatever `seal` returns. `seal` is
356    /// invoked once per `put` entry with its key, its plaintext record and the field names that
357    /// entry was staged as already sealed with (empty for anything staged through
358    /// [`WriteQueue::stage_put`]).
359    ///
360    /// A batch leaves the queue only once its append has landed. A refusal — the log turning the
361    /// append down, `seal` finding no key, the `sync` failing — abandons that batch and every one
362    /// still behind it: their claims are released and their keys kept for
363    /// [`WriteQueue::take_abandoned_keys`], so nothing of a write that never reached the log
364    /// survives as a pending claim a retry would conflict with. The batches that landed before it
365    /// stay committed; the `_seq` values the refused batch was stamped with stay burned.
366    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    /// Serializes the first `take` queued entries into log lines, stamping each with the next
414    /// `_seq` — the half of a commit that touches nothing on disk.
415    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    /// Throws away everything still queued after a batch was refused, releasing each claim and
456    /// keeping each key for [`WriteQueue::take_abandoned_keys`]. The batches behind the refused one
457    /// go too: they would be refused the same way, or land after the caller has already heard that
458    /// its flush failed.
459    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
467/// Appends one batch, one `append_lines_in` call per distinct partition it touches, and hands the
468/// slots back in the batch's own order. A batch that names a single partition — every batch of an
469/// unpartitioned table, and most batches of a partitioned one — makes exactly one append call, the
470/// same shape this queue had before partitions existed.
471fn 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
498/// Refuses a write that would move an existing key out of the partition it already lives in — the
499/// record's old line stays where it is, so the two files would both claim the key. Mirrors the TS
500/// `Table.#assertPartitionStable`: delete the record first, then write it into the new partition.
501pub(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
523/// What a batch of operations has already made of each key it named, as seen by an operation still
524/// being validated against the same batch: `Some(record)` for a key a put or patch will leave
525/// behind, `None` for one a delete will take away. Empty for a write that stands alone.
526pub(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
549/// True when the committed `holder` of a value has already been rewritten by this batch into
550/// something that no longer holds it — the committed index still names it, but the batch it belongs
551/// to will release it before anything lands, so it may not refuse `mine`.
552fn 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
565/// Every uniqueness rule the table enforces, checked against the committed registry **and** against
566/// `shadow` — the records earlier operations of the same batch will leave behind. A unit of work
567/// appends nothing until it commits, so without `shadow` two operations of one unit can each pass
568/// against the committed state and still duplicate each other (ADR-3h3r).
569pub(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, &registry, &json!({ "id": "u1", "name": "Ania" }), false)
691            .unwrap();
692        queue
693            .stage_put(&schema, &registry, &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, &registry, &json!({ "id": "u1", "name": "Ania" }), false)
714            .unwrap();
715        let err = queue
716            .stage_put(&schema, &registry, &json!({ "id": "u1", "name": "Zosia" }), false)
717            .unwrap_err();
718        assert!(matches!(err, StageError::Conflict(_)));
719
720        queue
721            .stage_put(&schema, &registry, &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, &registry, &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, &registry, &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    /// A log that refuses every append from the `failing_from`-th call on — a disk that filled up,
757    /// a file swapped out under the handle.
758    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, &registry, &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, &registry, &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, &registry, &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                &registry,
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                &registry,
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                &registry,
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                &registry,
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, &registry, &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, &registry, &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                    &registry,
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}