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            branch: request.branch.clone(),
257            request_id: request.request_id.clone(),
258            fingerprint,
259            version: next,
260        };
261        self.commit_rebuilt_counts(&name, &record, &at, receipt, counts)
262    }
263
264    fn commit_rebuilt_counts(
265        &mut self,
266        name: &str,
267        record: &BranchRefRecord,
268        at: &Captured,
269        receipt: Receipt,
270        counts: BTreeMap<CountBucket, BucketCount>,
271    ) -> Result<Receipt, Failure> {
272        self.rows_before_advance(&at.branch, record)?;
273        let branch =
274            open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
275        for (bucket, counter) in counts {
276            let key = bucket.key();
277            let shard = self.writer.shard_for(&key);
278            if counter.present {
279                branch.put(shard, key, counter.counted.to_string().into_bytes())
280            } else {
281                branch.delete(shard, key)
282            }
283            .map_err(Failure::branch)?;
284        }
285        branch
286            .put(self.writer.shard_for(READY_KEY), READY_KEY, READY_VALUE)
287            .map_err(Failure::branch)?;
288        let key = receipt_key(
289            &receipt.branch,
290            receipt.version.generation,
291            &receipt.request_id,
292        );
293        branch
294            .put(
295                self.writer.shard_for(&key),
296                key,
297                serde_json::to_vec(&receipt).map_err(Failure::storage)?,
298            )
299            .map_err(Failure::branch)?;
300        #[cfg(test)]
301        rebuild_probe::before_commit();
302        self.writer
303            .commit(
304                &branch,
305                &self.registry,
306                CommitRequest {
307                    durability: CommitDurability::Durable {
308                        refs: &mut self.refs,
309                    },
310                    extra_parents: &[],
311                    timestamp: current_timestamp(),
312                },
313            )
314            .map_err(|error| {
315                Failure::new(
316                    if self
317                        .refs
318                        .get(name)
319                        .is_some_and(|now| version(now) != at.version)
320                    {
321                        "durability_unknown"
322                    } else {
323                        "write_refused"
324                    },
325                    error.to_string(),
326                )
327            })?;
328        self.publish_record(name)?;
329        Ok(receipt)
330    }
331
332    fn refuse_count_merge(&self, at: &Captured) -> Result<(), Failure> {
333        if reads::get_bytes(&self.reader, at, b"b\0merge-candidate-progress")?.is_some() {
334            return Err(Failure::new(
335                "counts_merge_pending",
336                "branch holds a merge candidate",
337            ));
338        }
339        let service = self.merge_service_selection()?;
340        reads::scan_rows(&self.reader, &service, b"j\0merge\0", 1, |parts, bytes| {
341            let plan: MergePlan = serde_json::from_slice(bytes).map_err(Failure::storage)?;
342            identifier(&plan.id)?;
343            for pin in [&plan.source, &plan.target, &plan.origin] {
344                identifier(&pin.branch)?;
345            }
346            if Hash::of(plan.id.as_bytes()).to_string() != parts[0]
347                || plan.origin.branch != plan.target.branch
348            {
349                return Err(Failure::new(
350                    "merge_corrupt",
351                    "merge plan key differs from identity",
352                ));
353            }
354            if plan.state == MergeState::Open
355                && [&plan.source, &plan.target, &plan.origin]
356                    .iter()
357                    .any(|pin| pin.branch == at.branch)
358            {
359                return Err(Failure::new(
360                    "counts_merge_pending",
361                    "branch has an open merge",
362                ));
363            }
364            Ok(())
365        })
366    }
367
368    /// Read-only comparison; withheld callers receive no bucket identities or numbers.
369    ///
370    /// # Errors
371    /// Propagates corrupt metadata, malformed keys and content read failures.
372    pub fn audit_counts(
373        access: &Access,
374        reader: &ReadOnlyDatabase,
375        at: &Captured,
376    ) -> Result<CountsAudit, Failure> {
377        if !access.can_read_store() {
378            return Ok(CountsAudit::CannotTell {
379                reason: CountReason::Withheld,
380            });
381        }
382        if !is_ready(reader, at)? {
383            return Ok(CountsAudit::CannotTell {
384                reason: CountReason::CountsNotReady,
385            });
386        }
387        let mut counts = counted_buckets(reader, at)?;
388        count_rows(reader, at, &mut counts, true)?;
389        Ok(CountsAudit::Compared {
390            buckets: counts
391                .into_iter()
392                .map(|(bucket, count)| {
393                    let verdict = if count.kept == Some(count.counted) {
394                        AuditVerdict::Agree
395                    } else {
396                        AuditVerdict::Differ {
397                            kept: count.kept,
398                            counted: count.counted,
399                        }
400                    };
401                    AuditedBucket { bucket, verdict }
402                })
403                .collect(),
404        })
405    }
406
407    #[cfg(test)]
408    pub(in crate::serve) fn arm_count_rebuild_pause(
409        paused: std::sync::mpsc::Sender<()>,
410        release: std::sync::mpsc::Receiver<()>,
411    ) {
412        rebuild_probe::arm(paused, release);
413    }
414}
415
416fn rebuild_buckets(
417    reader: &ReadOnlyDatabase,
418    at: &Captured,
419) -> Result<BTreeMap<CountBucket, BucketCount>, Failure> {
420    let mut counts = counted_buckets(reader, at).map_err(|error| count_scan_failure(&error))?;
421    count_rows(reader, at, &mut counts, false).map_err(|error| count_scan_failure(&error))?;
422    Ok(counts)
423}
424
425fn count_scan_failure(error: &Failure) -> Failure {
426    Failure::new(
427        "counts_scan_refused",
428        format!("{}: {}", error.code, error.message),
429    )
430}
431
432#[cfg(test)]
433pub(super) mod rebuild_probe {
434    use super::Failure;
435    use std::cell::{Cell, RefCell};
436    use std::sync::mpsc::{Receiver, Sender};
437    thread_local! {
438        static CRASH: Cell<bool> = const { Cell::new(false) };
439        static PAUSE: RefCell<Option<(Sender<()>, Receiver<()>)>> = const { RefCell::new(None) };
440    }
441    pub fn crash_next() {
442        CRASH.with(|slot| slot.set(true));
443    }
444    pub fn arm(paused: Sender<()>, release: Receiver<()>) {
445        PAUSE.with(|slot| *slot.borrow_mut() = Some((paused, release)));
446    }
447    pub fn pause() -> Result<(), Failure> {
448        if let Some((paused, release)) = PAUSE.with(|slot| slot.borrow_mut().take()) {
449            paused.send(()).map_err(Failure::storage)?;
450            release.recv().map_err(Failure::storage)?;
451        }
452        Ok(())
453    }
454    pub fn before_commit() {
455        assert!(
456            !CRASH.with(|slot| slot.replace(false)),
457            "injected rebuild interruption"
458        );
459    }
460}
461
462pub(in crate::serve::storage) fn initialize_new_store(
463    writer: &mut BranchDatabase,
464    refs: &mut BranchRefStore,
465    registry: &BranchRegistry,
466) -> Result<(), Failure> {
467    let service = open_branch(SERVICE, refs, registry).map_err(Failure::branch_commit)?;
468    service
469        .put(writer.shard_for(READY_KEY), READY_KEY, READY_VALUE)
470        .map_err(Failure::branch)?;
471    writer
472        .commit(
473            &service,
474            registry,
475            CommitRequest {
476                durability: CommitDurability::Durable { refs },
477                extra_parents: &[],
478                timestamp: current_timestamp(),
479            },
480        )
481        .map_err(Failure::branch_commit)?;
482    Ok(())
483}
484
485fn document_count_key(collection: &str) -> Vec<u8> {
486    format!("k\0d\0{collection}").into_bytes()
487}
488
489fn read(reader: &ReadOnlyDatabase, at: &Captured, key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
490    #[cfg(test)]
491    probe::read(key);
492    reads::get_bytes(reader, at, key)
493}
494
495fn is_ready(reader: &ReadOnlyDatabase, at: &Captured) -> Result<bool, Failure> {
496    match read(reader, at, READY_KEY)? {
497        None => Ok(false),
498        Some(value) if value.as_slice() == READY_VALUE => Ok(true),
499        Some(_) => Err(Failure::new(
500            "counts_corrupt",
501            "count readiness format is invalid",
502        )),
503    }
504}
505
506fn decode(value: &[u8]) -> Result<u64, Failure> {
507    if value.is_empty()
508        || value.len() > 20
509        || (value.len() > 1 && value.first() == Some(&b'0'))
510        || !value.iter().all(u8::is_ascii_digit)
511    {
512        return Err(Failure::new(
513            "counts_corrupt",
514            "count is not a canonical decimal u64",
515        ));
516    }
517    std::str::from_utf8(value)
518        .map_err(|error| Failure::new("counts_corrupt", error.to_string()))?
519        .parse()
520        .map_err(|error: std::num::ParseIntError| Failure::new("counts_corrupt", error.to_string()))
521}
522
523fn required_documents(
524    reader: &ReadOnlyDatabase,
525    at: &Captured,
526    collection: &str,
527) -> Result<u64, Failure> {
528    let value = read(reader, at, &document_count_key(collection))?
529        .ok_or_else(|| Failure::new("counts_corrupt", "ready collection has no document count"))?;
530    decode(&value)
531}
532
533fn document_exists(
534    reader: &ReadOnlyDatabase,
535    at: &Captured,
536    collection: &str,
537    id: &str,
538) -> Result<bool, Failure> {
539    let key = revision_key(collection, id);
540    Ok(read(reader, at, &key)?.is_some())
541}
542
543fn increased(count: u64, added: u64) -> Result<Option<u64>, Failure> {
544    if added == 0 {
545        return Ok(None);
546    }
547    added_count(count, added).map(Some)
548}
549
550fn added_count(count: u64, added: u64) -> Result<u64, Failure> {
551    count
552        .checked_add(added)
553        .ok_or_else(|| Failure::new("count_overflow", "object count exceeds u64"))
554}
555
556fn removed_count(count: u64, removed: u64) -> Result<u64, Failure> {
557    count
558        .checked_sub(removed)
559        .ok_or_else(|| Failure::new("count_underflow", "removed objects exceed count"))
560}
561
562pub(super) fn append(
563    reader: &ReadOnlyDatabase,
564    at: &Captured,
565    change: &Change,
566    writes: &mut Vec<PreparedWrite>,
567) -> Result<(), Failure> {
568    let collection = match change {
569        Change::Collection(write) => Some(&write.collection),
570        Change::Put(write) => Some(&write.collection),
571        Change::AppendPrune(write) => Some(&write.collection),
572        Change::Delete(write) => Some(&write.collection),
573        Change::DeleteMany(delete) => Some(&delete.collection),
574        Change::PutMany(import) => Some(&import.collection),
575        Change::EdgePut(_)
576        | Change::EdgeDelete(_)
577        | Change::EdgePutMany(_)
578        | Change::VectorPut(_)
579        | Change::VectorPutMany(_)
580        | Change::VectorDelete(_)
581        | Change::RelationPut(_) => None,
582        _ => return Ok(()),
583    };
584    if !is_ready(reader, at)? {
585        return Ok(());
586    }
587    if let Some(collection) = collection {
588        append_documents(reader, at, change, collection, writes)?;
589    }
590    let mut counters = declared_counters(reader, at, writes)?;
591    append_objects(reader, at, writes, &mut counters)?;
592    for (key, counter) in counters {
593        if counter.write {
594            writes.push((key, Some(counter.value.to_string().into_bytes())));
595        }
596    }
597    #[cfg(test)]
598    probe::write_count(writes);
599    Ok(())
600}
601
602fn append_documents(
603    reader: &ReadOnlyDatabase,
604    at: &Captured,
605    change: &Change,
606    collection: &str,
607    writes: &mut Vec<PreparedWrite>,
608) -> Result<(), Failure> {
609    let count = match change {
610        Change::Collection(_) => {
611            if read(reader, at, &collection_key(collection))?.is_some() {
612                required_documents(reader, at, collection)?;
613                None
614            } else {
615                if read(reader, at, &document_count_key(collection))?.is_some() {
616                    return Err(Failure::new(
617                        "counts_corrupt",
618                        "document count has no collection",
619                    ));
620                }
621                Some(0)
622            }
623        }
624        Change::Put(write) => {
625            let present = document_exists(reader, at, collection, &write.document_id)?;
626            increased(
627                required_documents(reader, at, collection)?,
628                u64::from(!present),
629            )?
630        }
631        Change::Delete(write) => {
632            if document_exists(reader, at, collection, &write.document_id)? {
633                Some(removed_count(
634                    required_documents(reader, at, collection)?,
635                    1,
636                )?)
637            } else {
638                None
639            }
640        }
641        Change::DeleteMany(delete) => Some(removed_count(
642            required_documents(reader, at, collection)?,
643            u64::try_from(delete.document_ids.len()).map_err(Failure::storage)?,
644        )?),
645        Change::AppendPrune(_) => {
646            let prefix = revision_key(collection, "");
647            let mut added = 0_u64;
648            let mut removed = 0_u64;
649            for (key, value) in writes.iter() {
650                let Some(id) = key.strip_prefix(prefix.as_slice()) else {
651                    continue;
652                };
653                let id = std::str::from_utf8(id).map_err(Failure::storage)?;
654                let present = document_exists(reader, at, collection, id)?;
655                if value.is_some() && !present {
656                    added = added_count(added, 1)?;
657                }
658                if value.is_none() && present {
659                    removed = added_count(removed, 1)?;
660                }
661            }
662            Some(added_count(
663                removed_count(required_documents(reader, at, collection)?, removed)?,
664                added,
665            )?)
666        }
667        Change::PutMany(import) => {
668            let mut added = 0_u64;
669            for document in &import.documents {
670                if !document_exists(reader, at, collection, &document.document_id)? {
671                    added = added.checked_add(1).ok_or_else(|| {
672                        Failure::new("count_overflow", "import delta exceeds u64")
673                    })?;
674                }
675            }
676            increased(required_documents(reader, at, collection)?, added)?
677        }
678        _ => return Ok(()),
679    };
680    if let Some(count) = count {
681        writes.push((
682            document_count_key(collection),
683            Some(count.to_string().into_bytes()),
684        ));
685    }
686    Ok(())
687}
688
689#[derive(Default)]
690struct Delta {
691    added: u64,
692    removed: u64,
693    existing: bool,
694}
695
696struct Counter {
697    value: u64,
698    write: bool,
699    initialized: bool,
700}
701
702pub(super) struct CandidateCounts {
703    ready: bool,
704    changes: BTreeMap<Vec<u8>, (bool, bool)>,
705}
706
707impl CandidateCounts {
708    pub(super) fn new(reader: &ReadOnlyDatabase, at: &Captured) -> Result<Self, Failure> {
709        Ok(Self {
710            ready: is_ready(reader, at)?,
711            changes: BTreeMap::new(),
712        })
713    }
714
715    pub(super) fn record(
716        &mut self,
717        reader: &ReadOnlyDatabase,
718        at: &Captured,
719        key: &[u8],
720        presence_key: &[u8],
721        after: bool,
722    ) -> Result<(), Failure> {
723        if !self.ready {
724            return Ok(());
725        }
726        if let Some(change) = self.changes.get_mut(key) {
727            change.1 = after;
728        } else {
729            let before = read(reader, at, presence_key)?.is_some();
730            self.changes.insert(key.to_vec(), (before, after));
731        }
732        Ok(())
733    }
734
735    pub(super) fn append(
736        self,
737        reader: &ReadOnlyDatabase,
738        at: &Captured,
739        rows: &mut BTreeMap<Vec<u8>, Option<Vec<u8>>>,
740    ) -> Result<(), Failure> {
741        if !self.ready {
742            return Ok(());
743        }
744        let mut counters = BTreeMap::new();
745        for (key, value) in &*rows {
746            let Some(value) = value else { continue };
747            if key.starts_with(b"w\0")
748                && matches!(
749                    super::embedded::read_field(value)?,
750                    super::embedded::Field::Embedded(_)
751                )
752            {
753                continue;
754            }
755            let count_key = if let Some(name) = key.strip_prefix(b"c\0") {
756                let collection = std::str::from_utf8(name).map_err(Failure::storage)?;
757                identifier(collection)?;
758                Some(document_count_key(collection))
759            } else {
760                declaration_count_key(key)?
761            };
762            let Some(count_key) = count_key else { continue };
763            let stored = read(reader, at, &count_key)?;
764            let count = match &stored {
765                Some(bytes) => decode(bytes)?,
766                None if read(reader, at, key)?.is_none() => 0,
767                None => return Err(Failure::new("counts_corrupt", "declared kind has no count")),
768            };
769            counters.insert(
770                count_key,
771                Counter {
772                    value: count,
773                    write: stored.is_none(),
774                    initialized: stored.is_none(),
775                },
776            );
777        }
778        let deltas = candidate_deltas(self.changes)?;
779        for (key, delta) in deltas {
780            let counter = match counters.entry(key) {
781                Entry::Occupied(entry) => entry.into_mut(),
782                Entry::Vacant(entry) => {
783                    let value = match read(reader, at, entry.key())? {
784                        Some(bytes) => decode(&bytes)?,
785                        None if !delta.existing => {
786                            candidate_missing_count(reader, at, entry.key())?
787                        }
788                        None => {
789                            return Err(Failure::new(
790                                "counts_corrupt",
791                                "present object has no count",
792                            ));
793                        }
794                    };
795                    entry.insert(Counter {
796                        value,
797                        write: false,
798                        initialized: false,
799                    })
800                }
801            };
802            if counter.initialized && delta.existing {
803                return Err(Failure::new(
804                    "counts_corrupt",
805                    "present object has no previous count",
806                ));
807            }
808            counter.value = added_count(removed_count(counter.value, delta.removed)?, delta.added)?;
809            counter.write |= delta.added != 0 || delta.removed != 0;
810        }
811        for (key, counter) in counters {
812            if counter.write {
813                rows.insert(key, Some(counter.value.to_string().into_bytes()));
814            }
815        }
816        Ok(())
817    }
818}
819
820fn candidate_deltas(
821    changes: BTreeMap<Vec<u8>, (bool, bool)>,
822) -> Result<BTreeMap<Vec<u8>, Delta>, Failure> {
823    let mut deltas: BTreeMap<Vec<u8>, Delta> = BTreeMap::new();
824    for (key, (before, after)) in changes {
825        let count_key = if let Some(suffix) = key.strip_prefix(b"d\0") {
826            let text = std::str::from_utf8(suffix).map_err(Failure::storage)?;
827            let (collection, document) = text
828                .split_once('\0')
829                .ok_or_else(|| Failure::new("corrupt_key", "document owner has no identifier"))?;
830            identifier(collection)?;
831            identifier(document)?;
832            document_count_key(collection)
833        } else {
834            object_count_key(&key)?
835                .ok_or_else(|| Failure::new("counts_corrupt", "candidate object has no bucket"))?
836        };
837        let delta = deltas.entry(count_key).or_default();
838        delta.existing |= before;
839        if after && !before {
840            delta.added = added_count(delta.added, 1)?;
841        }
842        if before && !after {
843            delta.removed = added_count(delta.removed, 1)?;
844        }
845    }
846    Ok(deltas)
847}
848
849fn candidate_missing_count(
850    reader: &ReadOnlyDatabase,
851    at: &Captured,
852    key: &[u8],
853) -> Result<u64, Failure> {
854    if let Some(collection) = key.strip_prefix(b"k\0d\0") {
855        let collection = std::str::from_utf8(collection).map_err(Failure::storage)?;
856        identifier(collection)?;
857        if read(reader, at, &collection_key(collection))?.is_some() {
858            return Err(Failure::new(
859                "counts_corrupt",
860                "declared collection has no count",
861            ));
862        }
863        Ok(0)
864    } else {
865        unregistered_count(reader, at, key)
866    }
867}
868
869fn declaration_count_key(key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
870    if let Some(name) = key.strip_prefix(b"n\0") {
871        let name = std::str::from_utf8(name).map_err(Failure::storage)?;
872        identifier(name)?;
873        return Ok(Some(format!("k\0e\0{name}").into_bytes()));
874    }
875    if !key.starts_with(b"w\0") {
876        return Ok(None);
877    }
878    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
879    let parts: Vec<_> = text.split('\0').collect();
880    let ["w", collection, field] = parts.as_slice() else {
881        return Err(invalid_vector_key());
882    };
883    identifier(collection)?;
884    identifier(field)?;
885    Ok(Some(format!("k\0v\0{collection}\0{field}").into_bytes()))
886}
887
888fn declared_counters(
889    reader: &ReadOnlyDatabase,
890    at: &Captured,
891    writes: &[PreparedWrite],
892) -> Result<BTreeMap<Vec<u8>, Counter>, Failure> {
893    let mut counters = BTreeMap::new();
894    for (key, value) in writes {
895        if value.is_none() {
896            continue;
897        }
898        let Some(count_key) = declaration_count_key(key)? else {
899            continue;
900        };
901        if counters.contains_key(&count_key) {
902            continue;
903        }
904        let declared = read(reader, at, key)?.is_some();
905        let counter = match read(reader, at, &count_key)? {
906            Some(value) => Counter {
907                value: decode(&value)?,
908                write: false,
909                initialized: false,
910            },
911            None if !declared => Counter {
912                value: 0,
913                write: true,
914                initialized: true,
915            },
916            None => return Err(Failure::new("counts_corrupt", "declared kind has no count")),
917        };
918        counters.insert(count_key, counter);
919    }
920    Ok(counters)
921}
922
923fn object_count_key(key: &[u8]) -> Result<Option<Vec<u8>>, Failure> {
924    if key.starts_with(b"e\0") {
925        let edge = edges::decode_key(key)?;
926        return Ok(Some(format!("k\0e\0{}", edge.relation).into_bytes()));
927    }
928    if !key.starts_with(b"v\0") {
929        return Ok(None);
930    }
931    let text = std::str::from_utf8(key).map_err(Failure::storage)?;
932    let mut parts = text.split('\0');
933    if parts.next() != Some("v") {
934        return Err(invalid_vector_key());
935    }
936    let collection = parts.next().ok_or_else(invalid_vector_key)?;
937    let field = parts.next().ok_or_else(invalid_vector_key)?;
938    let document = parts.next().ok_or_else(invalid_vector_key)?;
939    if parts.next().is_some() {
940        return Err(invalid_vector_key());
941    }
942    identifier(collection)?;
943    identifier(field)?;
944    identifier(document)?;
945    Ok(Some(format!("k\0v\0{collection}\0{field}").into_bytes()))
946}
947
948fn invalid_vector_key() -> Failure {
949    Failure::new("vector_key", "vector key must have three identifier parts")
950}
951
952fn append_objects(
953    reader: &ReadOnlyDatabase,
954    at: &Captured,
955    writes: &[PreparedWrite],
956    counters: &mut BTreeMap<Vec<u8>, Counter>,
957) -> Result<(), Failure> {
958    // Only the final effect of each canonical key changes a bucket count.
959    // Incoming edge mirrors and field declarations are not counted objects.
960    let mut final_presence = BTreeMap::new();
961    for (key, value) in writes {
962        if key.starts_with(b"e\0") || key.starts_with(b"v\0") {
963            final_presence.insert(key.as_slice(), value.is_some());
964        }
965    }
966    let mut deltas: BTreeMap<Vec<u8>, Delta> = BTreeMap::new();
967    for (key, present) in final_presence {
968        let count_key = object_count_key(key)?
969            .ok_or_else(|| Failure::new("counts_corrupt", "counted object has no bucket"))?;
970        let existed = read(reader, at, key)?.is_some();
971        let delta = deltas.entry(count_key).or_default();
972        delta.existing |= existed;
973        if present && !existed {
974            delta.added = delta
975                .added
976                .checked_add(1)
977                .ok_or_else(|| Failure::new("count_overflow", "object delta exceeds u64"))?;
978        } else if existed && !present {
979            delta.removed = delta
980                .removed
981                .checked_add(1)
982                .ok_or_else(|| Failure::new("count_overflow", "object delta exceeds u64"))?;
983        }
984    }
985    for (key, delta) in deltas {
986        if let Some(counter) = counters.get_mut(&key) {
987            if counter.initialized && delta.existing {
988                return Err(Failure::new(
989                    "counts_corrupt",
990                    "unregistered kind has stored objects",
991                ));
992            }
993            counter.value = added_count(removed_count(counter.value, delta.removed)?, delta.added)?;
994            counter.write |= delta.added != 0 || delta.removed != 0;
995            continue;
996        }
997        let count = match read(reader, at, &key)? {
998            Some(value) => decode(&value)?,
999            None if !delta.existing => unregistered_count(reader, at, &key)?,
1000            None => {
1001                return Err(Failure::new(
1002                    "counts_corrupt",
1003                    "present object has no count",
1004                ));
1005            }
1006        };
1007        let remaining = removed_count(count, delta.removed)?;
1008        let next = added_count(remaining, delta.added)?;
1009        if delta.added != 0 || delta.removed != 0 {
1010            counters.insert(
1011                key,
1012                Counter {
1013                    value: next,
1014                    write: true,
1015                    initialized: false,
1016                },
1017            );
1018        }
1019    }
1020    Ok(())
1021}
1022
1023fn unregistered_count(
1024    reader: &ReadOnlyDatabase,
1025    at: &Captured,
1026    key: &[u8],
1027) -> Result<u64, Failure> {
1028    let declaration = if let Some(name) = key.strip_prefix(b"k\0e\0") {
1029        let mut declaration = b"n\0".to_vec();
1030        declaration.extend_from_slice(name);
1031        declaration
1032    } else if let Some(field) = key.strip_prefix(b"k\0v\0") {
1033        let mut declaration = b"w\0".to_vec();
1034        declaration.extend_from_slice(field);
1035        declaration
1036    } else {
1037        return Err(Failure::new(
1038            "counts_corrupt",
1039            "count key has no declaration family",
1040        ));
1041    };
1042    if read(reader, at, &declaration)?.is_some() {
1043        return Err(Failure::new("counts_corrupt", "declared kind has no count"));
1044    }
1045    Ok(0)
1046}
1047
1048pub(super) fn sum_rows(
1049    reader: &ReadOnlyDatabase,
1050    at: &Captured,
1051    prefix: &[u8],
1052    parts: usize,
1053) -> Result<(u64, std::collections::BTreeSet<String>), Failure> {
1054    let mut total = 0;
1055    let mut names = std::collections::BTreeSet::new();
1056    reads::scan_rows(reader, at, prefix, parts, |parts, value| {
1057        let name = parts.join("\0");
1058        if !names.insert(name) {
1059            return Err(Failure::new(
1060                "counts_corrupt",
1061                "count exists in multiple shards",
1062            ));
1063        }
1064        total = added_count(total, decode(value)?)?;
1065        Ok(())
1066    })?;
1067    Ok((total, names))
1068}
1069
1070#[cfg(test)]
1071mod probe {
1072    use std::cell::Cell;
1073
1074    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1075    pub(super) struct Reads {
1076        pub(super) ready: usize,
1077        pub(super) revisions: usize,
1078        pub(super) bodies: usize,
1079        pub(super) objects: usize,
1080        pub(super) counts: usize,
1081        pub(super) count_writes: usize,
1082    }
1083
1084    std::thread_local! {
1085        static READS: Cell<Reads> = const { Cell::new(Reads {
1086            ready: 0, revisions: 0, bodies: 0, objects: 0,
1087            counts: 0, count_writes: 0,
1088        }) };
1089    }
1090
1091    pub(super) fn read(key: &[u8]) {
1092        READS.with(|slot| {
1093            let mut value = slot.get();
1094            if key == super::READY_KEY {
1095                value.ready += 1;
1096            } else if key.starts_with(b"m\0") {
1097                value.revisions += 1;
1098            } else if key.starts_with(b"d\0") {
1099                value.bodies += 1;
1100            } else if key.starts_with(b"e\0") || key.starts_with(b"v\0") {
1101                value.objects += 1;
1102            } else if key.starts_with(b"k\0") {
1103                value.counts += 1;
1104            }
1105            slot.set(value);
1106        });
1107    }
1108
1109    pub(super) fn write_count(writes: &[super::PreparedWrite]) {
1110        READS.with(|slot| {
1111            let mut value = slot.get();
1112            value.count_writes += writes
1113                .iter()
1114                .filter(|(key, _)| key.starts_with(b"k\0"))
1115                .count();
1116            slot.set(value);
1117        });
1118    }
1119
1120    pub(super) fn take() -> Reads {
1121        READS.with(Cell::take)
1122    }
1123}
1124
1125#[cfg(test)]
1126#[path = "counts_tests.rs"]
1127mod tests;