Skip to main content

haematite_cli/serve/storage/
counts.rs

1//! Counts move with semantic writes in one durable branch version.
2//! Served document writes carry no expiry, so their presence at a version is fixed.
3//! Unready branches keep ordinary writes and never invent count metadata.
4
5use std::collections::{BTreeMap, btree_map::Entry};
6
7use haematite::branch::{CommitDurability, CommitRequest, current_timestamp};
8use haematite::db::{ReadOnlyDatabase, branches::BranchDatabase};
9use haematite::tree::Hash;
10
11use super::mutations::PreparedWrite;
12use super::{
13    BranchRefRecord, BranchRefStore, BranchRegistry, Captured, Failure, MergePlan, MergeState,
14    Receipt, SERVICE, Storage, branch_name, collection_key, edges, identifier, open_branch, reads,
15    receipt_key, revision_key, version,
16};
17use crate::serve::access::Access;
18use crate::serve::protocol::{
19    AuditVerdict, AuditedBucket, Change, CountBucket, CountReason, CountsAudit, CountsRebuild,
20    KeptCount, ReceiptQuery, Version,
21};
22
23pub(super) struct View<'a> {
24    reader: &'a ReadOnlyDatabase,
25    at: &'a Captured,
26    ready: bool,
27}
28
29impl<'a> View<'a> {
30    pub(super) fn new(reader: &'a ReadOnlyDatabase, at: &'a Captured) -> Result<Self, Failure> {
31        Ok(Self {
32            reader,
33            at,
34            ready: is_ready(reader, at)?,
35        })
36    }
37
38    pub(super) const fn unavailable(&self, allowed: bool) -> Option<CountReason> {
39        if !self.ready {
40            Some(CountReason::CountsNotReady)
41        } else if !allowed {
42            Some(CountReason::Withheld)
43        } else {
44            None
45        }
46    }
47
48    pub(super) fn documents(
49        &self,
50        access: &Access,
51        collection: &str,
52    ) -> Result<KeptCount, Failure> {
53        if let Some(reason) = self.unavailable(access.can_read_collection(collection)) {
54            return Ok(KeptCount {
55                value: None,
56                reason: Some(reason),
57            });
58        }
59        Ok(KeptCount {
60            value: Some(required_documents(self.reader, self.at, collection)?),
61            reason: None,
62        })
63    }
64
65    pub(super) fn relationships(
66        &self,
67        access: &Access,
68        relation: &str,
69    ) -> Result<KeptCount, Failure> {
70        if let Some(reason) = self.unavailable(access.can_read_store()) {
71            return Ok(KeptCount {
72                value: None,
73                reason: Some(reason),
74            });
75        }
76        let key = format!("k\0e\0{relation}");
77        let value = read(self.reader, self.at, key.as_bytes())?
78            .ok_or_else(|| Failure::new("counts_corrupt", "declared relation has no count"))?;
79        Ok(KeptCount {
80            value: Some(decode(&value)?),
81            reason: None,
82        })
83    }
84}
85
86const READY_KEY: &[u8] = b"k\0ready";
87const READY_VALUE: &[u8] = b"1";
88
89#[derive(Default)]
90struct BucketCount {
91    counted: u64,
92    kept: Option<u64>,
93    present: bool,
94}
95
96impl CountBucket {
97    fn key(&self) -> Vec<u8> {
98        match self {
99            Self::Documents { collection } => document_count_key(collection),
100            Self::Relationships { relation } => format!("k\0e\0{relation}").into_bytes(),
101            Self::Vectors { collection, field } => {
102                format!("k\0v\0{collection}\0{field}").into_bytes()
103            }
104        }
105    }
106}
107
108fn counted_buckets(
109    reader: &ReadOnlyDatabase,
110    at: &Captured,
111) -> Result<BTreeMap<CountBucket, BucketCount>, Failure> {
112    let mut counts = BTreeMap::new();
113    for (prefix, parts, kind) in [
114        (b"c\0".as_slice(), 1, 0),
115        (b"n\0".as_slice(), 1, 1),
116        (b"w\0".as_slice(), 2, 2),
117        (b"d\0".as_slice(), 2, 3),
118        (b"e\0".as_slice(), 5, 4),
119        (b"v\0".as_slice(), 3, 5),
120    ] {
121        reads::scan_rows(reader, at, prefix, parts, |parts, bytes| {
122            let bucket = match kind {
123                0 | 3 => CountBucket::Documents {
124                    collection: parts[0].to_owned(),
125                },
126                1 => {
127                    super::relations::decode(parts[0], bytes)?;
128                    CountBucket::Relationships {
129                        relation: parts[0].to_owned(),
130                    }
131                }
132                4 => CountBucket::Relationships {
133                    relation: parts[2].to_owned(),
134                },
135                _ => {
136                    if kind == 2
137                        && matches!(
138                            super::embedded::read_field(bytes)?,
139                            super::embedded::Field::Embedded(_)
140                        )
141                    {
142                        return Ok(());
143                    }
144                    CountBucket::Vectors {
145                        collection: parts[0].to_owned(),
146                        field: parts[1].to_owned(),
147                    }
148                }
149            };
150            let counter: &mut BucketCount = counts.entry(bucket).or_default();
151            counter.present = true;
152            if kind >= 3 {
153                counter.counted = added_count(counter.counted, 1)?;
154            }
155            Ok(())
156        })?;
157    }
158    Ok(counts)
159}
160
161fn count_rows(
162    reader: &ReadOnlyDatabase,
163    at: &Captured,
164    counts: &mut BTreeMap<CountBucket, BucketCount>,
165    audit: bool,
166) -> Result<(), Failure> {
167    for (prefix, parts, kind) in [
168        (b"k\0d\0".as_slice(), 1, 0),
169        (b"k\0e\0".as_slice(), 1, 1),
170        (b"k\0v\0".as_slice(), 2, 2),
171    ] {
172        reads::scan_rows(reader, at, prefix, parts, |parts, bytes| {
173            let bucket = match kind {
174                0 => CountBucket::Documents {
175                    collection: parts[0].to_owned(),
176                },
177                1 => CountBucket::Relationships {
178                    relation: parts[0].to_owned(),
179                },
180                _ => CountBucket::Vectors {
181                    collection: parts[0].to_owned(),
182                    field: parts[1].to_owned(),
183                },
184            };
185            let counter = counts.entry(bucket).or_default();
186            if audit && counter.kept.replace(decode(bytes)?).is_some() {
187                return Err(Failure::new("counts_corrupt", "duplicate count bucket"));
188            }
189            Ok(())
190        })?;
191    }
192    Ok(())
193}
194
195impl Storage {
196    /// Counts visible content and installs all counters with one branch version.
197    ///
198    /// # Errors
199    /// Refuses incomplete operations and scan errors before staging; commit errors
200    /// preserve the common mutation outcome and receipt reconciliation rules.
201    pub fn rebuild_counts(&mut self, request: &CountsRebuild) -> Result<Receipt, Failure> {
202        identifier(&request.branch)?;
203        identifier(&request.request_id)?;
204        let name = branch_name(&request.branch);
205        if self.pending_discards.contains(&name) {
206            return Err(Failure::new(
207                "counts_discard_pending",
208                "branch discard is incomplete",
209            ));
210        }
211        let record = self
212            .refs
213            .get(&name)
214            .ok_or_else(|| Failure::new("counts_branch_missing", &request.branch))?
215            .clone();
216        let fingerprint =
217            Hash::of(&serde_json::to_vec(&("counts.rebuild", request)).map_err(Failure::storage)?)
218                .to_string();
219        if let Some(receipt) = self.receipt(&ReceiptQuery {
220            branch: request.branch.clone(),
221            request_id: request.request_id.clone(),
222        })? {
223            return if receipt.fingerprint == fingerprint {
224                Ok(receipt)
225            } else {
226                Err(Failure::new(
227                    "request_conflict",
228                    "request identity has different content",
229                ))
230            };
231        }
232        let at = Captured {
233            branch: request.branch.clone(),
234            version: version(&record),
235            shards: record.shards.clone(),
236        };
237        self.refuse_count_merge(&at).map_err(|error| {
238            if error.code == "counts_merge_pending" {
239                error
240            } else {
241                count_scan_failure(&error)
242            }
243        })?;
244        let next = Version {
245            generation: record.created,
246            sequence: record
247                .seq
248                .checked_add(1)
249                .ok_or_else(|| Failure::new("sequence_exhausted", "branch sequence exhausted"))?,
250        };
251        let counts = rebuild_buckets(&self.reader, &at)?;
252        #[cfg(test)]
253        rebuild_probe::pause()?;
254        let receipt = Receipt {
255            retention: None,
256            versions_released: None,
257            branch: request.branch.clone(),
258            request_id: request.request_id.clone(),
259            fingerprint,
260            version: next,
261        };
262        self.commit_rebuilt_counts(&name, &record, &at, receipt, counts)
263    }
264
265    fn commit_rebuilt_counts(
266        &mut self,
267        name: &str,
268        record: &BranchRefRecord,
269        at: &Captured,
270        receipt: Receipt,
271        counts: BTreeMap<CountBucket, BucketCount>,
272    ) -> Result<Receipt, Failure> {
273        self.rows_before_advance(&at.branch, record)?;
274        let branch =
275            open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
276        for (bucket, counter) in counts {
277            let key = bucket.key();
278            let shard = self.writer.shard_for(&key);
279            if counter.present {
280                branch.put(shard, key, counter.counted.to_string().into_bytes())
281            } else {
282                branch.delete(shard, key)
283            }
284            .map_err(Failure::branch)?;
285        }
286        branch
287            .put(self.writer.shard_for(READY_KEY), READY_KEY, READY_VALUE)
288            .map_err(Failure::branch)?;
289        let key = receipt_key(
290            &receipt.branch,
291            receipt.version.generation,
292            &receipt.request_id,
293        );
294        branch
295            .put(
296                self.writer.shard_for(&key),
297                key,
298                serde_json::to_vec(&receipt).map_err(Failure::storage)?,
299            )
300            .map_err(Failure::branch)?;
301        #[cfg(test)]
302        rebuild_probe::before_commit();
303        self.writer
304            .commit(
305                &branch,
306                &self.registry,
307                CommitRequest {
308                    durability: CommitDurability::Durable {
309                        refs: &mut self.refs,
310                    },
311                    extra_parents: &[],
312                    timestamp: current_timestamp(),
313                    note: None,
314                },
315            )
316            .map_err(|error| {
317                Failure::new(
318                    if self
319                        .refs
320                        .get(name)
321                        .is_some_and(|now| version(now) != at.version)
322                    {
323                        "durability_unknown"
324                    } else {
325                        "write_refused"
326                    },
327                    error.to_string(),
328                )
329            })?;
330        self.publish_record(name)?;
331        Ok(receipt)
332    }
333
334    fn refuse_count_merge(&self, at: &Captured) -> Result<(), Failure> {
335        if reads::get_bytes(&self.reader, at, b"b\0merge-candidate-progress")?.is_some() {
336            return Err(Failure::new(
337                "counts_merge_pending",
338                "branch holds a merge candidate",
339            ));
340        }
341        let service = self.merge_service_selection()?;
342        reads::scan_rows(&self.reader, &service, b"j\0merge\0", 1, |parts, bytes| {
343            let plan: MergePlan = serde_json::from_slice(bytes).map_err(Failure::storage)?;
344            identifier(&plan.id)?;
345            for pin in [&plan.source, &plan.target, &plan.origin] {
346                identifier(&pin.branch)?;
347            }
348            if Hash::of(plan.id.as_bytes()).to_string() != parts[0]
349                || plan.origin.branch != plan.target.branch
350            {
351                return Err(Failure::new(
352                    "merge_corrupt",
353                    "merge plan key differs from identity",
354                ));
355            }
356            if plan.state == MergeState::Open
357                && [&plan.source, &plan.target, &plan.origin]
358                    .iter()
359                    .any(|pin| pin.branch == at.branch)
360            {
361                return Err(Failure::new(
362                    "counts_merge_pending",
363                    "branch has an open merge",
364                ));
365            }
366            Ok(())
367        })
368    }
369
370    /// Read-only comparison; withheld callers receive no bucket identities or numbers.
371    ///
372    /// # Errors
373    /// Propagates corrupt metadata, malformed keys and content read failures.
374    pub fn audit_counts(
375        access: &Access,
376        reader: &ReadOnlyDatabase,
377        at: &Captured,
378    ) -> Result<CountsAudit, Failure> {
379        if !access.can_read_store() {
380            return Ok(CountsAudit::CannotTell {
381                reason: CountReason::Withheld,
382            });
383        }
384        if !is_ready(reader, at)? {
385            return Ok(CountsAudit::CannotTell {
386                reason: CountReason::CountsNotReady,
387            });
388        }
389        let mut counts = counted_buckets(reader, at)?;
390        count_rows(reader, at, &mut counts, true)?;
391        Ok(CountsAudit::Compared {
392            buckets: counts
393                .into_iter()
394                .map(|(bucket, count)| {
395                    let verdict = if count.kept == Some(count.counted) {
396                        AuditVerdict::Agree
397                    } else {
398                        AuditVerdict::Differ {
399                            kept: count.kept,
400                            counted: count.counted,
401                        }
402                    };
403                    AuditedBucket { bucket, verdict }
404                })
405                .collect(),
406        })
407    }
408
409    #[cfg(test)]
410    pub(in crate::serve) fn arm_count_rebuild_pause(
411        paused: std::sync::mpsc::Sender<()>,
412        release: std::sync::mpsc::Receiver<()>,
413    ) {
414        rebuild_probe::arm(paused, release);
415    }
416}
417
418fn rebuild_buckets(
419    reader: &ReadOnlyDatabase,
420    at: &Captured,
421) -> Result<BTreeMap<CountBucket, BucketCount>, Failure> {
422    let mut counts = counted_buckets(reader, at).map_err(|error| count_scan_failure(&error))?;
423    count_rows(reader, at, &mut counts, false).map_err(|error| count_scan_failure(&error))?;
424    Ok(counts)
425}
426
427fn count_scan_failure(error: &Failure) -> Failure {
428    Failure::new(
429        "counts_scan_refused",
430        format!("{}: {}", error.code, error.message),
431    )
432}
433
434#[cfg(test)]
435pub(super) mod rebuild_probe {
436    use super::Failure;
437    use std::cell::{Cell, RefCell};
438    use std::sync::mpsc::{Receiver, Sender};
439    thread_local! {
440        static CRASH: Cell<bool> = const { Cell::new(false) };
441        static PAUSE: RefCell<Option<(Sender<()>, Receiver<()>)>> = const { RefCell::new(None) };
442    }
443    pub fn crash_next() {
444        CRASH.with(|slot| slot.set(true));
445    }
446    pub fn arm(paused: Sender<()>, release: Receiver<()>) {
447        PAUSE.with(|slot| *slot.borrow_mut() = Some((paused, release)));
448    }
449    pub fn pause() -> Result<(), Failure> {
450        if let Some((paused, release)) = PAUSE.with(|slot| slot.borrow_mut().take()) {
451            paused.send(()).map_err(Failure::storage)?;
452            release.recv().map_err(Failure::storage)?;
453        }
454        Ok(())
455    }
456    pub fn before_commit() {
457        assert!(
458            !CRASH.with(|slot| slot.replace(false)),
459            "injected rebuild interruption"
460        );
461    }
462}
463
464pub(in crate::serve::storage) fn initialize_new_store(
465    writer: &mut BranchDatabase,
466    refs: &mut BranchRefStore,
467    registry: &BranchRegistry,
468) -> Result<(), Failure> {
469    let service = open_branch(SERVICE, refs, registry).map_err(Failure::branch_commit)?;
470    service
471        .put(writer.shard_for(READY_KEY), READY_KEY, READY_VALUE)
472        .map_err(Failure::branch)?;
473    writer
474        .commit(
475            &service,
476            registry,
477            CommitRequest {
478                durability: CommitDurability::Durable { refs },
479                extra_parents: &[],
480                timestamp: current_timestamp(),
481                note: None,
482            },
483        )
484        .map_err(Failure::branch_commit)?;
485    Ok(())
486}
487
488fn document_count_key(collection: &str) -> Vec<u8> {
489    format!("k\0d\0{collection}").into_bytes()
490}
491
492fn read(reader: &ReadOnlyDatabase, at: &Captured, key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
493    #[cfg(test)]
494    probe::read(key);
495    reads::get_bytes(reader, at, key)
496}
497
498fn is_ready(reader: &ReadOnlyDatabase, at: &Captured) -> Result<bool, Failure> {
499    match read(reader, at, READY_KEY)? {
500        None => Ok(false),
501        Some(value) if value.as_slice() == READY_VALUE => Ok(true),
502        Some(_) => Err(Failure::new(
503            "counts_corrupt",
504            "count readiness format is invalid",
505        )),
506    }
507}
508
509fn decode(value: &[u8]) -> Result<u64, Failure> {
510    if value.is_empty()
511        || value.len() > 20
512        || (value.len() > 1 && value.first() == Some(&b'0'))
513        || !value.iter().all(u8::is_ascii_digit)
514    {
515        return Err(Failure::new(
516            "counts_corrupt",
517            "count is not a canonical decimal u64",
518        ));
519    }
520    std::str::from_utf8(value)
521        .map_err(|error| Failure::new("counts_corrupt", error.to_string()))?
522        .parse()
523        .map_err(|error: std::num::ParseIntError| Failure::new("counts_corrupt", error.to_string()))
524}
525
526fn required_documents(
527    reader: &ReadOnlyDatabase,
528    at: &Captured,
529    collection: &str,
530) -> Result<u64, Failure> {
531    let value = read(reader, at, &document_count_key(collection))?
532        .ok_or_else(|| Failure::new("counts_corrupt", "ready collection has no document count"))?;
533    decode(&value)
534}
535
536fn document_exists(
537    reader: &ReadOnlyDatabase,
538    at: &Captured,
539    collection: &str,
540    id: &str,
541) -> Result<bool, Failure> {
542    let key = revision_key(collection, id);
543    Ok(read(reader, at, &key)?.is_some())
544}
545
546fn increased(count: u64, added: u64) -> Result<Option<u64>, Failure> {
547    if added == 0 {
548        return Ok(None);
549    }
550    added_count(count, added).map(Some)
551}
552
553fn added_count(count: u64, added: u64) -> Result<u64, Failure> {
554    count
555        .checked_add(added)
556        .ok_or_else(|| Failure::new("count_overflow", "object count exceeds u64"))
557}
558
559fn removed_count(count: u64, removed: u64) -> Result<u64, Failure> {
560    count
561        .checked_sub(removed)
562        .ok_or_else(|| Failure::new("count_underflow", "removed objects exceed count"))
563}
564
565pub(super) fn append(
566    reader: &ReadOnlyDatabase,
567    at: &Captured,
568    change: &Change,
569    writes: &mut Vec<PreparedWrite>,
570) -> Result<(), Failure> {
571    let collection = match change {
572        Change::Collection(write) => Some(&write.collection),
573        Change::Put(write) => Some(&write.collection),
574        Change::AppendPrune(write) => Some(&write.collection),
575        Change::Delete(write) => Some(&write.collection),
576        Change::DeleteMany(delete) => Some(&delete.collection),
577        Change::PutMany(import) => Some(&import.collection),
578        Change::EdgePut(_)
579        | Change::EdgeDelete(_)
580        | Change::EdgePutMany(_)
581        | Change::VectorPut(_)
582        | Change::VectorPutMany(_)
583        | Change::VectorDelete(_)
584        | Change::RelationPut(_) => None,
585        _ => return Ok(()),
586    };
587    if !is_ready(reader, at)? {
588        return Ok(());
589    }
590    if let Some(collection) = collection {
591        append_documents(reader, at, change, collection, writes)?;
592    }
593    let mut counters = declared_counters(reader, at, writes)?;
594    append_objects(reader, at, writes, &mut counters)?;
595    for (key, counter) in counters {
596        if counter.write {
597            writes.push((key, Some(counter.value.to_string().into_bytes())));
598        }
599    }
600    #[cfg(test)]
601    probe::write_count(writes);
602    Ok(())
603}
604
605fn append_documents(
606    reader: &ReadOnlyDatabase,
607    at: &Captured,
608    change: &Change,
609    collection: &str,
610    writes: &mut Vec<PreparedWrite>,
611) -> Result<(), Failure> {
612    let count = match change {
613        Change::Collection(_) => {
614            if read(reader, at, &collection_key(collection))?.is_some() {
615                required_documents(reader, at, collection)?;
616                None
617            } else {
618                if read(reader, at, &document_count_key(collection))?.is_some() {
619                    return Err(Failure::new(
620                        "counts_corrupt",
621                        "document count has no collection",
622                    ));
623                }
624                Some(0)
625            }
626        }
627        Change::Put(write) => {
628            let present = document_exists(reader, at, collection, &write.document_id)?;
629            increased(
630                required_documents(reader, at, collection)?,
631                u64::from(!present),
632            )?
633        }
634        Change::Delete(write) => {
635            if document_exists(reader, at, collection, &write.document_id)? {
636                Some(removed_count(
637                    required_documents(reader, at, collection)?,
638                    1,
639                )?)
640            } else {
641                None
642            }
643        }
644        Change::DeleteMany(delete) => Some(removed_count(
645            required_documents(reader, at, collection)?,
646            u64::try_from(delete.document_ids.len()).map_err(Failure::storage)?,
647        )?),
648        Change::AppendPrune(_) => {
649            let prefix = revision_key(collection, "");
650            let mut added = 0_u64;
651            let mut removed = 0_u64;
652            for (key, value) in writes.iter() {
653                let Some(id) = key.strip_prefix(prefix.as_slice()) else {
654                    continue;
655                };
656                let id = std::str::from_utf8(id).map_err(Failure::storage)?;
657                let present = document_exists(reader, at, collection, id)?;
658                if value.is_some() && !present {
659                    added = added_count(added, 1)?;
660                }
661                if value.is_none() && present {
662                    removed = added_count(removed, 1)?;
663                }
664            }
665            Some(added_count(
666                removed_count(required_documents(reader, at, collection)?, removed)?,
667                added,
668            )?)
669        }
670        Change::PutMany(import) => {
671            let mut added = 0_u64;
672            for document in &import.documents {
673                if !document_exists(reader, at, collection, &document.document_id)? {
674                    added = added.checked_add(1).ok_or_else(|| {
675                        Failure::new("count_overflow", "import delta exceeds u64")
676                    })?;
677                }
678            }
679            increased(required_documents(reader, at, collection)?, added)?
680        }
681        _ => return Ok(()),
682    };
683    if let Some(count) = count {
684        writes.push((
685            document_count_key(collection),
686            Some(count.to_string().into_bytes()),
687        ));
688    }
689    Ok(())
690}
691
692#[derive(Default)]
693struct Delta {
694    added: u64,
695    removed: u64,
696    existing: bool,
697}
698
699struct Counter {
700    value: u64,
701    write: bool,
702    initialized: bool,
703}
704
705pub(super) struct CandidateCounts {
706    ready: bool,
707    changes: BTreeMap<Vec<u8>, (bool, bool)>,
708}
709
710impl CandidateCounts {
711    pub(super) fn new(reader: &ReadOnlyDatabase, at: &Captured) -> Result<Self, Failure> {
712        Ok(Self {
713            ready: is_ready(reader, at)?,
714            changes: BTreeMap::new(),
715        })
716    }
717
718    pub(super) fn record(
719        &mut self,
720        reader: &ReadOnlyDatabase,
721        at: &Captured,
722        key: &[u8],
723        presence_key: &[u8],
724        after: bool,
725    ) -> Result<(), Failure> {
726        if !self.ready {
727            return Ok(());
728        }
729        if let Some(change) = self.changes.get_mut(key) {
730            change.1 = after;
731        } else {
732            let before = read(reader, at, presence_key)?.is_some();
733            self.changes.insert(key.to_vec(), (before, after));
734        }
735        Ok(())
736    }
737
738    pub(super) fn append(
739        self,
740        reader: &ReadOnlyDatabase,
741        at: &Captured,
742        rows: &mut BTreeMap<Vec<u8>, Option<Vec<u8>>>,
743    ) -> Result<(), Failure> {
744        if !self.ready {
745            return Ok(());
746        }
747        let mut counters = BTreeMap::new();
748        for (key, value) in &*rows {
749            let Some(value) = value else { continue };
750            if key.starts_with(b"w\0")
751                && matches!(
752                    super::embedded::read_field(value)?,
753                    super::embedded::Field::Embedded(_)
754                )
755            {
756                continue;
757            }
758            let count_key = if let Some(name) = key.strip_prefix(b"c\0") {
759                let collection = std::str::from_utf8(name).map_err(Failure::storage)?;
760                identifier(collection)?;
761                Some(document_count_key(collection))
762            } else {
763                declaration_count_key(key)?
764            };
765            let Some(count_key) = count_key else { continue };
766            let stored = read(reader, at, &count_key)?;
767            let count = match &stored {
768                Some(bytes) => decode(bytes)?,
769                None if read(reader, at, key)?.is_none() => 0,
770                None => return Err(Failure::new("counts_corrupt", "declared kind has no count")),
771            };
772            counters.insert(
773                count_key,
774                Counter {
775                    value: count,
776                    write: stored.is_none(),
777                    initialized: stored.is_none(),
778                },
779            );
780        }
781        let deltas = candidate_deltas(self.changes)?;
782        for (key, delta) in deltas {
783            let counter = match counters.entry(key) {
784                Entry::Occupied(entry) => entry.into_mut(),
785                Entry::Vacant(entry) => {
786                    let value = match read(reader, at, entry.key())? {
787                        Some(bytes) => decode(&bytes)?,
788                        None if !delta.existing => {
789                            candidate_missing_count(reader, at, entry.key())?
790                        }
791                        None => {
792                            return Err(Failure::new(
793                                "counts_corrupt",
794                                "present object has no count",
795                            ));
796                        }
797                    };
798                    entry.insert(Counter {
799                        value,
800                        write: false,
801                        initialized: false,
802                    })
803                }
804            };
805            if counter.initialized && delta.existing {
806                return Err(Failure::new(
807                    "counts_corrupt",
808                    "present object has no previous count",
809                ));
810            }
811            counter.value = added_count(removed_count(counter.value, delta.removed)?, delta.added)?;
812            counter.write |= delta.added != 0 || delta.removed != 0;
813        }
814        for (key, counter) in counters {
815            if counter.write {
816                rows.insert(key, Some(counter.value.to_string().into_bytes()));
817            }
818        }
819        Ok(())
820    }
821}
822
823fn candidate_deltas(
824    changes: BTreeMap<Vec<u8>, (bool, bool)>,
825) -> Result<BTreeMap<Vec<u8>, Delta>, Failure> {
826    let mut deltas: BTreeMap<Vec<u8>, Delta> = BTreeMap::new();
827    for (key, (before, after)) in changes {
828        let count_key = if let Some(suffix) = key.strip_prefix(b"d\0") {
829            let text = std::str::from_utf8(suffix).map_err(Failure::storage)?;
830            let (collection, document) = text
831                .split_once('\0')
832                .ok_or_else(|| Failure::new("corrupt_key", "document owner has no identifier"))?;
833            identifier(collection)?;
834            identifier(document)?;
835            document_count_key(collection)
836        } else {
837            object_count_key(&key)?
838                .ok_or_else(|| Failure::new("counts_corrupt", "candidate object has no bucket"))?
839        };
840        let delta = deltas.entry(count_key).or_default();
841        delta.existing |= before;
842        if after && !before {
843            delta.added = added_count(delta.added, 1)?;
844        }
845        if before && !after {
846            delta.removed = added_count(delta.removed, 1)?;
847        }
848    }
849    Ok(deltas)
850}
851
852fn candidate_missing_count(
853    reader: &ReadOnlyDatabase,
854    at: &Captured,
855    key: &[u8],
856) -> Result<u64, Failure> {
857    if let Some(collection) = key.strip_prefix(b"k\0d\0") {
858        let collection = std::str::from_utf8(collection).map_err(Failure::storage)?;
859        identifier(collection)?;
860        if read(reader, at, &collection_key(collection))?.is_some() {
861            return Err(Failure::new(
862                "counts_corrupt",
863                "declared collection has no count",
864            ));
865        }
866        Ok(0)
867    } else {
868        unregistered_count(reader, at, key)
869    }
870}
871
872fn declaration_count_key(key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
873    if let Some(name) = key.strip_prefix(b"n\0") {
874        let name = std::str::from_utf8(name).map_err(Failure::storage)?;
875        identifier(name)?;
876        return Ok(Some(format!("k\0e\0{name}").into_bytes()));
877    }
878    if !key.starts_with(b"w\0") {
879        return Ok(None);
880    }
881    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
882    let parts: Vec<_> = text.split('\0').collect();
883    let ["w", collection, field] = parts.as_slice() else {
884        return Err(invalid_vector_key());
885    };
886    identifier(collection)?;
887    identifier(field)?;
888    Ok(Some(format!("k\0v\0{collection}\0{field}").into_bytes()))
889}
890
891fn declared_counters(
892    reader: &ReadOnlyDatabase,
893    at: &Captured,
894    writes: &[PreparedWrite],
895) -> Result<BTreeMap<Vec<u8>, Counter>, Failure> {
896    let mut counters = BTreeMap::new();
897    for (key, value) in writes {
898        if value.is_none() {
899            continue;
900        }
901        let Some(count_key) = declaration_count_key(key)? else {
902            continue;
903        };
904        if counters.contains_key(&count_key) {
905            continue;
906        }
907        let declared = read(reader, at, key)?.is_some();
908        let counter = match read(reader, at, &count_key)? {
909            Some(value) => Counter {
910                value: decode(&value)?,
911                write: false,
912                initialized: false,
913            },
914            None if !declared => Counter {
915                value: 0,
916                write: true,
917                initialized: true,
918            },
919            None => return Err(Failure::new("counts_corrupt", "declared kind has no count")),
920        };
921        counters.insert(count_key, counter);
922    }
923    Ok(counters)
924}
925
926fn object_count_key(key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
927    if key.starts_with(b"e\0") {
928        let edge = edges::decode_key(key)?;
929        return Ok(Some(format!("k\0e\0{}", edge.relation).into_bytes()));
930    }
931    if !key.starts_with(b"v\0") {
932        return Ok(None);
933    }
934    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
935    let mut parts = text.split('\0');
936    if parts.next() != Some("v") {
937        return Err(invalid_vector_key());
938    }
939    let collection = parts.next().ok_or_else(invalid_vector_key)?;
940    let field = parts.next().ok_or_else(invalid_vector_key)?;
941    let document = parts.next().ok_or_else(invalid_vector_key)?;
942    if parts.next().is_some() {
943        return Err(invalid_vector_key());
944    }
945    identifier(collection)?;
946    identifier(field)?;
947    identifier(document)?;
948    Ok(Some(format!("k\0v\0{collection}\0{field}").into_bytes()))
949}
950
951fn invalid_vector_key() -> Failure {
952    Failure::new("vector_key", "vector key must have three identifier parts")
953}
954
955fn append_objects(
956    reader: &ReadOnlyDatabase,
957    at: &Captured,
958    writes: &[PreparedWrite],
959    counters: &mut BTreeMap<Vec<u8>, Counter>,
960) -> Result<(), Failure> {
961    // Only the final effect of each canonical key changes a bucket count.
962    // Incoming edge mirrors and field declarations are not counted objects.
963    let mut final_presence = BTreeMap::new();
964    for (key, value) in writes {
965        if key.starts_with(b"e\0") || key.starts_with(b"v\0") {
966            final_presence.insert(key.as_slice(), value.is_some());
967        }
968    }
969    let mut deltas: BTreeMap<Vec<u8>, Delta> = BTreeMap::new();
970    for (key, present) in final_presence {
971        let count_key = object_count_key(key)?
972            .ok_or_else(|| Failure::new("counts_corrupt", "counted object has no bucket"))?;
973        let existed = read(reader, at, key)?.is_some();
974        let delta = deltas.entry(count_key).or_default();
975        delta.existing |= existed;
976        if present && !existed {
977            delta.added = delta
978                .added
979                .checked_add(1)
980                .ok_or_else(|| Failure::new("count_overflow", "object delta exceeds u64"))?;
981        } else if existed && !present {
982            delta.removed = delta
983                .removed
984                .checked_add(1)
985                .ok_or_else(|| Failure::new("count_overflow", "object delta exceeds u64"))?;
986        }
987    }
988    for (key, delta) in deltas {
989        if let Some(counter) = counters.get_mut(&key) {
990            if counter.initialized && delta.existing {
991                return Err(Failure::new(
992                    "counts_corrupt",
993                    "unregistered kind has stored objects",
994                ));
995            }
996            counter.value = added_count(removed_count(counter.value, delta.removed)?, delta.added)?;
997            counter.write |= delta.added != 0 || delta.removed != 0;
998            continue;
999        }
1000        let count = match read(reader, at, &key)? {
1001            Some(value) => decode(&value)?,
1002            None if !delta.existing => unregistered_count(reader, at, &key)?,
1003            None => {
1004                return Err(Failure::new(
1005                    "counts_corrupt",
1006                    "present object has no count",
1007                ));
1008            }
1009        };
1010        let remaining = removed_count(count, delta.removed)?;
1011        let next = added_count(remaining, delta.added)?;
1012        if delta.added != 0 || delta.removed != 0 {
1013            counters.insert(
1014                key,
1015                Counter {
1016                    value: next,
1017                    write: true,
1018                    initialized: false,
1019                },
1020            );
1021        }
1022    }
1023    Ok(())
1024}
1025
1026fn unregistered_count(
1027    reader: &ReadOnlyDatabase,
1028    at: &Captured,
1029    key: &[u8],
1030) -> Result<u64, Failure> {
1031    let declaration = if let Some(name) = key.strip_prefix(b"k\0e\0") {
1032        let mut declaration = b"n\0".to_vec();
1033        declaration.extend_from_slice(name);
1034        declaration
1035    } else if let Some(field) = key.strip_prefix(b"k\0v\0") {
1036        let mut declaration = b"w\0".to_vec();
1037        declaration.extend_from_slice(field);
1038        declaration
1039    } else {
1040        return Err(Failure::new(
1041            "counts_corrupt",
1042            "count key has no declaration family",
1043        ));
1044    };
1045    if read(reader, at, &declaration)?.is_some() {
1046        return Err(Failure::new("counts_corrupt", "declared kind has no count"));
1047    }
1048    Ok(0)
1049}
1050
1051pub(super) fn sum_rows(
1052    reader: &ReadOnlyDatabase,
1053    at: &Captured,
1054    prefix: &[u8],
1055    parts: usize,
1056) -> Result<(u64, std::collections::BTreeSet<String>), Failure> {
1057    let mut total = 0;
1058    let mut names = std::collections::BTreeSet::new();
1059    reads::scan_rows(reader, at, prefix, parts, |parts, value| {
1060        let name = parts.join("\0");
1061        if !names.insert(name) {
1062            return Err(Failure::new(
1063                "counts_corrupt",
1064                "count exists in multiple shards",
1065            ));
1066        }
1067        total = added_count(total, decode(value)?)?;
1068        Ok(())
1069    })?;
1070    Ok((total, names))
1071}
1072
1073#[cfg(test)]
1074mod probe {
1075    use std::cell::Cell;
1076
1077    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1078    pub(super) struct Reads {
1079        pub(super) ready: usize,
1080        pub(super) revisions: usize,
1081        pub(super) bodies: usize,
1082        pub(super) objects: usize,
1083        pub(super) counts: usize,
1084        pub(super) count_writes: usize,
1085    }
1086
1087    std::thread_local! {
1088        static READS: Cell<Reads> = const { Cell::new(Reads {
1089            ready: 0, revisions: 0, bodies: 0, objects: 0,
1090            counts: 0, count_writes: 0,
1091        }) };
1092    }
1093
1094    pub(super) fn read(key: &[u8]) {
1095        READS.with(|slot| {
1096            let mut value = slot.get();
1097            if key == super::READY_KEY {
1098                value.ready += 1;
1099            } else if key.starts_with(b"m\0") {
1100                value.revisions += 1;
1101            } else if key.starts_with(b"d\0") {
1102                value.bodies += 1;
1103            } else if key.starts_with(b"e\0") || key.starts_with(b"v\0") {
1104                value.objects += 1;
1105            } else if key.starts_with(b"k\0") {
1106                value.counts += 1;
1107            }
1108            slot.set(value);
1109        });
1110    }
1111
1112    pub(super) fn write_count(writes: &[super::PreparedWrite]) {
1113        READS.with(|slot| {
1114            let mut value = slot.get();
1115            value.count_writes += writes
1116                .iter()
1117                .filter(|(key, _)| key.starts_with(b"k\0"))
1118                .count();
1119            slot.set(value);
1120        });
1121    }
1122
1123    pub(super) fn take() -> Reads {
1124        READS.with(Cell::take)
1125    }
1126}
1127
1128#[cfg(test)]
1129#[path = "counts_tests.rs"]
1130mod tests;