Skip to main content

nedb_engine/
refs.rs

1// SPDX-FileCopyrightText: 2026 INTERCHAINED LLC
2// SPDX-License-Identifier: BUSL-1.1
3// NEDB · © 2026 INTERCHAINED LLC × Eth-Interchained × Vex (Claude Opus 5)
4
5//! Named pointers into history: mutable **refs** and immutable **tags**.
6//!
7//! ```text
8//! _nedb.refs   name -> seq     may move       (a branch head)
9//! _nedb.tags   name -> seq     never moves    (a release)
10//! ```
11//!
12//! # Why two kinds and not one with a flag
13//!
14//! Both are "a name pointing at a sequence", and it is tempting to make one
15//! type with `immutable: bool`. The reason they are two collections is that
16//! the guarantees are what the caller is buying. `set_ref` is allowed to
17//! surprise you; `create_tag` is not. Keeping them apart means an operation on
18//! a tag cannot be reached by accident from ref code, and `list_tags` cannot
19//! ever return something mutable.
20//!
21//! # What immutability actually costs
22//!
23//! A tag whose target can change is not a tag, it is a ref with good manners:
24//! a build that recorded "built from v1.0" would no longer be reproducible,
25//! and nothing in the record would say it had moved. So re-pointing a tag is
26//! refused, and — the sharper rule — a tag NAME IS NOT REUSABLE AFTER DELETION.
27//!
28//! Deletion has to exist (a tag published by mistake must be retractable), but
29//! if `v1.0` could be deleted and recreated at a different sequence, then
30//! "built from v1.0" is temporally ambiguous again and nothing has been
31//! gained — the mutation just takes two commands instead of one. So a delete
32//! leaves an AUDITED TOMBSTONE: the record stays, carrying the name, the
33//! original target, and `deleted: true`. That tombstone is both the audit
34//! trail and the enforcement mechanism, which is deliberate — there is no way
35//! to lose the ban without also losing the evidence.
36//!
37//! Refs are the deliberate contrast: they move, they can be deleted, and a
38//! deleted ref name can be reused. The `prev` chain on the underlying document
39//! gives every move a walkable history for free.
40//!
41//! # Reserved, therefore not part of state
42//!
43//! Both collections live under `_nedb.`, and they are written with
44//! `put_unchecked`, which does not register them in the collection registry.
45//! So they are invisible to [`crate::db::Db::state_root`]. That is required,
46//! not incidental: a tag records a state root, and if creating one changed the
47//! state root it would invalidate what it just recorded.
48
49use std::sync::atomic::Ordering;
50
51use anyhow::{bail, Result};
52
53use crate::db::Db;
54
55/// Mutable named pointers. Ids are ref names.
56pub const REFS: &str = "_nedb.refs";
57
58/// Immutable named pointers, including tombstones for deleted ones. Ids are
59/// tag names; a name present here is spent forever.
60pub const TAGS: &str = "_nedb.tags";
61
62// ── Records ───────────────────────────────────────────────────────────────
63
64/// An immutable pointer at a sequence.
65#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66pub struct TagRecord {
67    pub name: String,
68    /// The sequence this tag names. Fixed at creation, for all time.
69    pub at_seq: u64,
70    /// The `state_root` hex at `at_seq`, when a root was already persisted
71    /// there. `None` means no root existed — NOT that the state had none.
72    /// See [`create_tag`] for why one is not computed on the spot.
73    pub state_root: Option<String>,
74    /// The sequence at which the tag itself was created. Distinct from
75    /// `at_seq`: a tag can be applied to the past.
76    pub created_seq: u64,
77    /// A tombstone. The record is retained precisely so this can be true.
78    pub deleted: bool,
79    pub message: Option<String>,
80}
81
82/// A mutable pointer at a sequence.
83#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84pub struct RefRecord {
85    pub name: String,
86    pub at_seq: u64,
87    /// The sequence at which the ref last moved.
88    pub updated_seq: u64,
89}
90
91/// Internal shape of a ref document. `deleted` is not on [`RefRecord`] because
92/// a deleted ref is simply absent to every reader — unlike a tag, nothing
93/// downstream depends on knowing the name was once used.
94#[derive(serde::Serialize, serde::Deserialize)]
95struct StoredRef {
96    name: String,
97    at_seq: u64,
98    updated_seq: u64,
99    #[serde(default)]
100    deleted: bool,
101}
102
103// ── Name validation ───────────────────────────────────────────────────────
104
105/// Is this a name a ref or tag can HAVE?
106///
107/// Deliberately separate from [`crate::namespace::validate_name`]: a
108/// collection name becomes a directory, a ref name becomes a CLI argument, and
109/// the failure modes do not overlap.
110///
111/// Every rule here refuses rather than sanitises. A name silently rewritten is
112/// a different pointer than the one the caller asked for, and they would find
113/// out from the wrong build artifact rather than from an error.
114pub fn validate_ref_name(name: &str) -> Result<()> {
115    if name.is_empty() {
116        bail!("ref/tag name is empty");
117    }
118    if name.len() > 255 {
119        bail!("ref/tag name is {} bytes; the limit is 255", name.len());
120    }
121    if name.contains('/') || name.contains('\\') {
122        bail!(
123            "ref/tag name {:?} contains a path separator — the name is an id in a \
124             reserved collection, and a separator makes it look like a namespace \
125             that does not exist",
126            name
127        );
128    }
129    if name.contains('\0') {
130        bail!("ref/tag name {:?} contains a NUL byte", name);
131    }
132    if name != name.trim() {
133        bail!(
134            "ref/tag name {:?} has leading or trailing whitespace — refused rather \
135             than trimmed, because the name you get back must be the name you gave",
136            name
137        );
138    }
139    // The non-obvious one. Anywhere a revision is accepted — `nedb tag inspect
140    // <rev>`, `AS OF <rev>` — the argument is either a sequence number or a
141    // name, and the resolver has to be TOTAL: exactly one meaning per input,
142    // decided without context. A tag literally called "42" makes `42` mean two
143    // things, and any tie-break (prefer the number? prefer the name?) is a
144    // silent wrong answer for whoever meant the other one. Refusing the name is
145    // the only resolution that never guesses.
146    if name.bytes().all(|b| b.is_ascii_digit()) {
147        bail!(
148            "ref/tag name {:?} is purely numeric — it would be ambiguous with a \
149             sequence number wherever a revision is accepted, and an argument \
150             resolver must not guess which was meant",
151            name
152        );
153    }
154    Ok(())
155}
156
157// ── Tags ──────────────────────────────────────────────────────────────────
158
159/// Read the stored record for a name, tombstones included.
160///
161/// The single door to tag existence. Every rule in this module — no re-point,
162/// no reuse after delete, idempotent re-create — is decided from what this
163/// returns, so there is exactly one place the ban could be lost.
164fn read_tag_raw(db: &Db, name: &str) -> Option<TagRecord> {
165    let n = db.get(TAGS, name)?;
166    serde_json::from_value(n.data).ok()
167}
168
169fn write_tag(db: &Db, rec: &TagRecord) -> Result<()> {
170    db.put_unchecked(TAGS, &rec.name, serde_json::to_value(rec)?, vec![], None, None)?;
171    Ok(())
172}
173
174/// Create an immutable tag at `at_seq`.
175///
176/// Refuses, naming itself each time:
177///
178///   - an invalid name (see [`validate_ref_name`]);
179///   - a sequence that does not exist yet — a tag pointing into the future
180///     names nothing, and would silently become valid later, naming whatever
181///     happened to land there;
182///   - a name already tagged at a DIFFERENT sequence, reporting the current
183///     target so the caller can see what they collided with;
184///   - a name that was tagged and then deleted, reporting what it pointed at.
185///
186/// Re-creating the same name at the SAME sequence is idempotent success. The
187/// assertion the caller is making is already true, and tooling retries; an
188/// error there would be noise that teaches people to ignore this error class.
189///
190/// `state_root` is captured from [`Db::get_root`] when a root is already
191/// persisted at `at_seq`, and left `None` otherwise. It is NOT computed here:
192/// per `create_root_at`, a historical root is O(live state) plus a
193/// version-chain walk per document, and hiding that behind `tag` is how an
194/// operator learns the cost by waiting. `None` means "no root was taken here",
195/// and the caller can take one explicitly and re-tag at a new name.
196pub fn create_tag(db: &Db, name: &str, at_seq: u64, message: Option<&str>) -> Result<TagRecord> {
197    validate_ref_name(name)?;
198
199    // `seq` is the NEXT sequence to be assigned, so the tip is one below it.
200    let next_seq = db.seq.load(Ordering::SeqCst);
201    if at_seq >= next_seq {
202        bail!(
203            "cannot tag {:?} at sequence {}: the database is at sequence {} — a tag \
204             pointing into the future names no state, and would start naming \
205             whatever is written there later",
206            name,
207            at_seq,
208            next_seq.saturating_sub(1)
209        );
210    }
211
212    if let Some(existing) = read_tag_raw(db, name) {
213        if existing.deleted {
214            bail!(
215                "tag {:?} was previously deleted (it pointed at sequence {}) and its \
216                 name cannot be reused: a name that could be recreated at a different \
217                 target makes every past reference to it ambiguous — which {:?} did \
218                 that build use? Pick a new name.",
219                name,
220                existing.at_seq,
221                name
222            );
223        }
224        if existing.at_seq != at_seq {
225            bail!(
226                "tag {:?} already points at sequence {} and a tag target is \
227                 immutable; refusing to move it to {}. Use a ref if you want a \
228                 pointer that moves.",
229                name,
230                existing.at_seq,
231                at_seq
232            );
233        }
234        // Same name, same target: the assertion already holds. Idempotent.
235        return Ok(existing);
236    }
237
238    let state_root = db.get_root(at_seq).map(|r| r.root.state_root);
239    let rec = TagRecord {
240        name: name.to_string(),
241        at_seq,
242        state_root,
243        created_seq: next_seq,
244        deleted: false,
245        message: message.map(|s| s.to_string()),
246    };
247    write_tag(db, &rec)?;
248    Ok(rec)
249}
250
251/// A live tag by name. `None` for a name that was never tagged AND for one
252/// whose tag was deleted — both mean "no tag here now". Use
253/// [`get_tag_including_deleted`] to tell the two apart.
254pub fn get_tag(db: &Db, name: &str) -> Option<TagRecord> {
255    read_tag_raw(db, name).filter(|t| !t.deleted)
256}
257
258/// A tag by name, tombstones included. This is what answers "what did `v1.0`
259/// point at?" for a build that referenced it before it was retracted.
260pub fn get_tag_including_deleted(db: &Db, name: &str) -> Option<TagRecord> {
261    read_tag_raw(db, name)
262}
263
264fn all_tags(db: &Db) -> Vec<TagRecord> {
265    let mut out: Vec<TagRecord> = db
266        .id_index
267        .list_ids(TAGS)
268        .into_iter()
269        .filter_map(|id| read_tag_raw(db, &id))
270        .collect();
271    out.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
272    out
273}
274
275/// Live tags, sorted by name (raw UTF-8 bytes, the one ordering everything
276/// agrees on).
277pub fn list_tags(db: &Db) -> Vec<TagRecord> {
278    all_tags(db).into_iter().filter(|t| !t.deleted).collect()
279}
280
281/// Every tag ever created, tombstones included, sorted by name. The audit view.
282pub fn list_tags_including_deleted(db: &Db) -> Vec<TagRecord> {
283    all_tags(db)
284}
285
286/// Retract a tag, leaving an audited tombstone.
287///
288/// The record is REWRITTEN with `deleted: true`, not removed: it keeps the
289/// original `at_seq` so history stays answerable, and it is what makes the
290/// name permanently unusable. Returns `false` when there was no live tag to
291/// delete (never created, or already deleted) — that is not an error, but it
292/// is also not silence: the boolean is the answer.
293pub fn delete_tag(db: &Db, name: &str) -> Result<bool> {
294    validate_ref_name(name)?;
295    let existing = match read_tag_raw(db, name) {
296        None => return Ok(false),
297        Some(t) => t,
298    };
299    if existing.deleted {
300        return Ok(false);
301    }
302    // Every other field is preserved verbatim. A tombstone that forgot the
303    // target would be an audit record that answers nothing.
304    let rec = TagRecord { deleted: true, ..existing };
305    write_tag(db, &rec)?;
306    Ok(true)
307}
308
309// ── Refs ──────────────────────────────────────────────────────────────────
310
311fn read_ref_raw(db: &Db, name: &str) -> Option<StoredRef> {
312    let n = db.get(REFS, name)?;
313    serde_json::from_value(n.data).ok()
314}
315
316/// Point a ref at a sequence, creating it or MOVING it.
317///
318/// Moving is allowed and is the entire reason refs exist. It is still
319/// recorded: each move appends a new version of the same document, so the
320/// `prev` chain is a complete, walkable history of where this ref has been —
321/// the history comes free from the DAG rather than from a side log.
322///
323/// Future sequences are refused for the same reason as tags: a pointer at
324/// state that does not exist is not a pointer.
325pub fn set_ref(db: &Db, name: &str, at_seq: u64) -> Result<RefRecord> {
326    validate_ref_name(name)?;
327    let next_seq = db.seq.load(Ordering::SeqCst);
328    if at_seq >= next_seq {
329        bail!(
330            "cannot point ref {:?} at sequence {}: the database is at sequence {} — \
331             that state does not exist yet",
332            name,
333            at_seq,
334            next_seq.saturating_sub(1)
335        );
336    }
337    let rec = RefRecord { name: name.to_string(), at_seq, updated_seq: next_seq };
338    let stored = StoredRef {
339        name: rec.name.clone(),
340        at_seq: rec.at_seq,
341        updated_seq: rec.updated_seq,
342        deleted: false,
343    };
344    db.put_unchecked(REFS, name, serde_json::to_value(&stored)?, vec![], None, None)?;
345    Ok(rec)
346}
347
348/// A live ref by name.
349pub fn get_ref(db: &Db, name: &str) -> Option<RefRecord> {
350    let s = read_ref_raw(db, name)?;
351    if s.deleted {
352        return None;
353    }
354    Some(RefRecord { name: s.name, at_seq: s.at_seq, updated_seq: s.updated_seq })
355}
356
357/// Live refs, sorted by name.
358pub fn list_refs(db: &Db) -> Vec<RefRecord> {
359    let mut out: Vec<RefRecord> = db
360        .id_index
361        .list_ids(REFS)
362        .into_iter()
363        .filter_map(|id| get_ref(db, &id))
364        .collect();
365    out.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
366    out
367}
368
369/// Delete a ref. Returns `false` when there was no live ref by that name.
370///
371/// Written as a `deleted: true` version rather than a real delete for one
372/// mechanical reason and one design one: `Db::delete` refuses reserved
373/// collections outright, and keeping the version chain intact means the ref's
374/// movement history survives its deletion.
375///
376/// Unlike a tag, the NAME IS FREE AGAIN — [`set_ref`] will happily recreate
377/// it. That is the whole difference between the two kinds, and it is safe here
378/// because a ref never promised to stay put, so nothing downstream is entitled
379/// to assume a past reference to it still resolves the same way.
380pub fn delete_ref(db: &Db, name: &str) -> Result<bool> {
381    validate_ref_name(name)?;
382    let existing = match read_ref_raw(db, name) {
383        None => return Ok(false),
384        Some(s) => s,
385    };
386    if existing.deleted {
387        return Ok(false);
388    }
389    let stored = StoredRef { deleted: true, ..existing };
390    db.put_unchecked(REFS, name, serde_json::to_value(&stored)?, vec![], None, None)?;
391    Ok(true)
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use serde_json::json;
398    use tempfile::{tempdir, TempDir};
399
400    /// A database with `n` ordinary writes in it, so there are real sequences
401    /// to point at.
402    fn db_with(n: u64) -> (TempDir, Db) {
403        let dir = tempdir().unwrap();
404        let db = Db::open(dir.path(), None).unwrap();
405        for i in 0..n {
406            db.put("orders", &format!("o{}", i), json!({ "i": i }), vec![], None, None)
407                .unwrap();
408        }
409        (dir, db)
410    }
411
412    fn tip(db: &Db) -> u64 {
413        db.seq.load(Ordering::SeqCst).saturating_sub(1)
414    }
415
416    #[test]
417    fn a_tag_is_created_read_back_and_listed() {
418        let (_d, db) = db_with(3);
419        let t = create_tag(&db, "v1.0", 1, Some("first cut")).unwrap();
420        assert_eq!(t.at_seq, 1);
421        assert!(!t.deleted);
422        assert_eq!(t.message.as_deref(), Some("first cut"));
423
424        let got = get_tag(&db, "v1.0").expect("tag readable by name");
425        assert_eq!(got, t);
426
427        let listed = list_tags(&db);
428        assert_eq!(listed.len(), 1);
429        assert_eq!(listed[0].name, "v1.0");
430    }
431
432    #[test]
433    fn retagging_the_same_name_at_a_different_seq_is_refused_and_names_the_target() {
434        let (_d, db) = db_with(5);
435        create_tag(&db, "v1.0", 1, None).unwrap();
436        let err = create_tag(&db, "v1.0", 3, None).unwrap_err().to_string();
437        assert!(err.contains("already points at sequence 1"), "message was: {}", err);
438        assert!(err.contains("immutable"), "message was: {}", err);
439        // And the tag did NOT move.
440        assert_eq!(get_tag(&db, "v1.0").unwrap().at_seq, 1);
441    }
442
443    #[test]
444    fn retagging_the_same_name_at_the_same_seq_is_idempotent_success() {
445        let (_d, db) = db_with(5);
446        let first = create_tag(&db, "v1.0", 2, Some("m")).unwrap();
447        let again = create_tag(&db, "v1.0", 2, Some("different message")).unwrap();
448        // The original record is returned unchanged — a retry must not quietly
449        // rewrite the tag it was re-asserting.
450        assert_eq!(first, again);
451        assert_eq!(list_tags(&db).len(), 1);
452    }
453
454    #[test]
455    fn deleting_a_tag_leaves_an_audited_tombstone_with_its_target_intact() {
456        let (_d, db) = db_with(5);
457        create_tag(&db, "v1.0", 2, Some("oops")).unwrap();
458        assert!(delete_tag(&db, "v1.0").unwrap());
459
460        assert!(get_tag(&db, "v1.0").is_none(), "a deleted tag is not live");
461        assert!(list_tags(&db).is_empty());
462
463        let audit = list_tags_including_deleted(&db);
464        assert_eq!(audit.len(), 1, "the tombstone does not vanish");
465        assert!(audit[0].deleted);
466        assert_eq!(audit[0].at_seq, 2, "the tombstone remembers what it pointed at");
467        assert_eq!(audit[0].message.as_deref(), Some("oops"));
468        assert_eq!(get_tag_including_deleted(&db, "v1.0").unwrap().at_seq, 2);
469
470        // Deleting again is false, not an error, and not a second tombstone.
471        assert!(!delete_tag(&db, "v1.0").unwrap());
472        assert_eq!(list_tags_including_deleted(&db).len(), 1);
473    }
474
475    /// The sharpest rule in the module.
476    #[test]
477    fn a_deleted_tag_name_cannot_be_reused_at_any_target() {
478        let (_d, db) = db_with(5);
479        create_tag(&db, "v1.0", 2, None).unwrap();
480        delete_tag(&db, "v1.0").unwrap();
481
482        // Not at a different sequence...
483        let err = create_tag(&db, "v1.0", 4, None).unwrap_err().to_string();
484        assert!(err.contains("previously deleted"), "message was: {}", err);
485        assert!(err.contains("sequence 2"), "message must say what it pointed at: {}", err);
486
487        // ...and not at the SAME sequence either. Idempotency applies to a live
488        // tag being re-asserted, never to resurrecting a retracted one.
489        let err = create_tag(&db, "v1.0", 2, None).unwrap_err().to_string();
490        assert!(err.contains("previously deleted"), "message was: {}", err);
491
492        assert!(get_tag(&db, "v1.0").is_none());
493        assert_eq!(list_tags(&db).len(), 0);
494        // A different name at the same target is of course fine.
495        create_tag(&db, "v1.0.1", 2, None).unwrap();
496    }
497
498    #[test]
499    fn the_no_reuse_ban_survives_a_reopen() {
500        let dir = tempdir().unwrap();
501        {
502            let db = Db::open(dir.path(), None).unwrap();
503            db.put("orders", "a", json!({}), vec![], None, None).unwrap();
504            db.put("orders", "b", json!({}), vec![], None, None).unwrap();
505            create_tag(&db, "v1.0", 1, None).unwrap();
506            delete_tag(&db, "v1.0").unwrap();
507            db.flush_all();
508        }
509        let db = Db::open(dir.path(), None).unwrap();
510        let err = create_tag(&db, "v1.0", 1, None).unwrap_err().to_string();
511        assert!(err.contains("previously deleted"), "message was: {}", err);
512    }
513
514    #[test]
515    fn a_tag_captures_a_persisted_state_root_and_reports_none_when_there_is_not_one() {
516        let (_d, db) = db_with(4);
517        let at = tip(&db);
518        let untagged = create_tag(&db, "no-root", at, None).unwrap();
519        assert!(
520            untagged.state_root.is_none(),
521            "no root was persisted at {}, and the tag must say so rather than \
522             computing one behind the operator's back",
523            at
524        );
525
526        let persisted = db.create_root_at(at).unwrap();
527        let tagged = create_tag(&db, "has-root", at, None).unwrap();
528        assert_eq!(tagged.state_root.as_deref(), Some(persisted.root.state_root.as_str()));
529    }
530
531    #[test]
532    fn tagging_a_future_sequence_is_refused() {
533        let (_d, db) = db_with(3);
534        let next = db.seq.load(Ordering::SeqCst);
535        for future in [next, next + 1, u64::MAX] {
536            let err = create_tag(&db, "ahead", future, None).unwrap_err().to_string();
537            assert!(err.contains("future"), "message was: {}", err);
538        }
539        // The tip itself is fine.
540        create_tag(&db, "here", tip(&db), None).unwrap();
541    }
542
543    #[test]
544    fn invalid_names_are_refused_one_category_at_a_time() {
545        let (_d, db) = db_with(2);
546        let cases: &[(&str, &str)] = &[
547            ("", "empty"),
548            (" v1", "whitespace"),
549            ("v1 ", "whitespace"),
550            ("a/b", "path separator"),
551            ("a\\b", "path separator"),
552            ("a\0b", "NUL"),
553            ("42", "numeric"),
554            ("0", "numeric"),
555        ];
556        for (name, why) in cases {
557            let e = validate_ref_name(name)
558                .unwrap_err()
559                .to_string();
560            assert!(
561                e.to_lowercase().contains(&why.to_lowercase()),
562                "{:?} should be refused for {:?}, message was: {}", name, why, e
563            );
564            // And the refusal is enforced at every entry point, not just the
565            // validator — a rule only the validator knows is a rule the API
566            // does not have.
567            assert!(create_tag(&db, name, 0, None).is_err(), "create_tag({:?})", name);
568            assert!(set_ref(&db, name, 0).is_err(), "set_ref({:?})", name);
569            assert!(delete_tag(&db, name).is_err(), "delete_tag({:?})", name);
570            assert!(delete_ref(&db, name).is_err(), "delete_ref({:?})", name);
571        }
572
573        let too_long = "v".repeat(256);
574        assert!(validate_ref_name(&too_long).unwrap_err().to_string().contains("256 bytes"));
575
576        // Ordinary names, including ones with digits in them, still work.
577        for ok in ["v1.0", "main", "release-2026", "v42", "42a", "a b"] {
578            validate_ref_name(ok).unwrap_or_else(|e| panic!("{:?} refused: {}", ok, e));
579        }
580    }
581
582    #[test]
583    fn a_ref_is_set_moved_and_read_back() {
584        let (_d, db) = db_with(5);
585        let r = set_ref(&db, "main", 1).unwrap();
586        assert_eq!(r.at_seq, 1);
587        assert_eq!(get_ref(&db, "main").unwrap().at_seq, 1);
588
589        // Moving is allowed — this is the whole difference from a tag.
590        set_ref(&db, "main", 4).unwrap();
591        assert_eq!(get_ref(&db, "main").unwrap().at_seq, 4);
592
593        let listed = list_refs(&db);
594        assert_eq!(listed.len(), 1);
595        assert_eq!(listed[0].at_seq, 4);
596        assert!(get_ref(&db, "nope").is_none());
597    }
598
599    /// Both halves of the contrast in one place, because the difference is the
600    /// point and a test that only asserted one half would not document it.
601    #[test]
602    fn a_deleted_ref_name_is_reusable_but_a_deleted_tag_name_is_not() {
603        let (_d, db) = db_with(5);
604
605        set_ref(&db, "release", 1).unwrap();
606        assert!(delete_ref(&db, "release").unwrap());
607        assert!(get_ref(&db, "release").is_none());
608        assert!(list_refs(&db).is_empty());
609        assert!(!delete_ref(&db, "release").unwrap(), "already gone");
610        // Reused, at a DIFFERENT target, with no complaint.
611        set_ref(&db, "release", 3).unwrap();
612        assert_eq!(get_ref(&db, "release").unwrap().at_seq, 3);
613
614        create_tag(&db, "release", 1, None).unwrap();
615        delete_tag(&db, "release").unwrap();
616        assert!(
617            create_tag(&db, "release", 3, None).is_err(),
618            "the same sequence of operations that is legal for a ref must be \
619             refused for a tag"
620        );
621    }
622
623    #[test]
624    fn tags_and_refs_survive_a_reopen() {
625        let dir = tempdir().unwrap();
626        let root_hex;
627        {
628            let db = Db::open(dir.path(), None).unwrap();
629            for i in 0..4 {
630                db.put("orders", &format!("o{}", i), json!({ "i": i }), vec![], None, None)
631                    .unwrap();
632            }
633            let at = db.seq.load(Ordering::SeqCst) - 1;
634            root_hex = db.create_root_at(at).unwrap().root.state_root;
635            create_tag(&db, "v1.0", at, Some("ship it")).unwrap();
636            create_tag(&db, "v0.9", 1, None).unwrap();
637            delete_tag(&db, "v0.9").unwrap();
638            set_ref(&db, "main", 2).unwrap();
639            db.flush_all();
640        }
641
642        let db = Db::open(dir.path(), None).unwrap();
643        let t = get_tag(&db, "v1.0").expect("tag survived reopen");
644        assert_eq!(t.message.as_deref(), Some("ship it"));
645        assert_eq!(t.state_root.as_deref(), Some(root_hex.as_str()));
646        assert_eq!(list_tags(&db).len(), 1);
647        assert_eq!(list_tags_including_deleted(&db).len(), 2);
648        assert!(list_tags_including_deleted(&db).iter().any(|x| x.name == "v0.9" && x.deleted));
649        assert_eq!(get_ref(&db, "main").unwrap().at_seq, 2);
650        assert_eq!(list_refs(&db).len(), 1);
651    }
652
653    /// Refs and tags are engine records, so they must not be part of the state
654    /// they point at. If they were, tagging would change the root the tag just
655    /// captured.
656    #[test]
657    fn naming_a_state_does_not_change_it() {
658        let (_d, db) = db_with(4);
659        let before = db.state_root().unwrap();
660
661        create_tag(&db, "v1.0", 1, Some("a message long enough to matter")).unwrap();
662        set_ref(&db, "main", 2).unwrap();
663        delete_tag(&db, "v1.0").unwrap();
664        delete_ref(&db, "main").unwrap();
665
666        assert_eq!(db.state_root().unwrap(), before);
667        assert!(
668            !db.collections().iter().any(|c| c == TAGS || c == REFS),
669            "reserved collections must never enter the namespace"
670        );
671    }
672}