Skip to main content

cairn_core/
store.rs

1//! The content-addressed node store, backed by SQLite.
2//!
3//! Two tables. `nodes` maps a content hash to its canonical bytes; insertion is
4//! idempotent, so identical subtrees are stored exactly once. `refs` maps a
5//! name to a root hash: a `checkpoint` is an immutable named root, a `branch`
6//! is a movable one. This is the minimal store/ref surface v0.1 build step 1
7//! calls for (`docs/design.md` Section 8). SQLite is the bundled build, so
8//! there is no system dependency.
9
10use crate::node::{BinOp, Node, NodeHash, Param, Produces};
11use crate::ty::{Effect, Type};
12use rusqlite::{params, Connection, OptionalExtension};
13use std::collections::BTreeSet;
14use std::fmt;
15use std::path::Path;
16
17/// Errors the store can return.
18#[derive(Debug)]
19pub enum Error {
20    Sqlite(rusqlite::Error),
21    Decode(serde_json::Error),
22    /// A ref with this name already exists; checkpoints are immutable and a
23    /// branch may not clobber a checkpoint.
24    NameInUse(String),
25    /// A node referenced by hash is not present in the store.
26    MissingNode(NodeHash),
27    /// The store on disk was written by a different AST format version.
28    /// Pre-1.0 the node format is not stable; rather than silently
29    /// mis-decode an incompatible store, opening it is refused.
30    FormatMismatch { found: String, expected: u32 },
31}
32
33impl fmt::Display for Error {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Error::Sqlite(e) => write!(f, "sqlite error: {e}"),
37            Error::Decode(e) => write!(f, "node decode error: {e}"),
38            Error::NameInUse(n) => write!(f, "ref name already in use: {n}"),
39            Error::MissingNode(h) => write!(f, "missing node: {h}"),
40            Error::FormatMismatch { found, expected } => write!(
41                f,
42                "store format version {found} is not the supported {expected} \
43                 (pre-1.0 the AST format is not stable; this store must be \
44                 rebuilt with the current version)"
45            ),
46        }
47    }
48}
49
50impl std::error::Error for Error {}
51
52impl From<rusqlite::Error> for Error {
53    fn from(e: rusqlite::Error) -> Self {
54        Error::Sqlite(e)
55    }
56}
57
58impl From<serde_json::Error> for Error {
59    fn from(e: serde_json::Error) -> Self {
60        Error::Decode(e)
61    }
62}
63
64pub type Result<T> = std::result::Result<T, Error>;
65
66/// A subtree loaded back out of the store, children inlined, for inspection
67/// and equality checks.
68#[derive(Clone, PartialEq, Eq, Debug)]
69pub enum Materialized {
70    Lit(i64),
71    FloatLit(u64),
72    FloatOp {
73        op: BinOp,
74        lhs: Box<Materialized>,
75        rhs: Box<Materialized>,
76    },
77    IntToFloat(Box<Materialized>),
78    FloatToInt(Box<Materialized>),
79    DecimalLit(i64),
80    DecimalOp {
81        op: BinOp,
82        lhs: Box<Materialized>,
83        rhs: Box<Materialized>,
84    },
85    IntToDecimal(Box<Materialized>),
86    DecimalToInt(Box<Materialized>),
87    DecimalRaw(Box<Materialized>),
88    Bool(bool),
89    Not(Box<Materialized>),
90    Str(String),
91    StrLen(Box<Materialized>),
92    StrLower(Box<Materialized>),
93    StrFromCode(Box<Materialized>),
94    StrConcat(Box<Materialized>, Box<Materialized>),
95    StrSlice {
96        s: Box<Materialized>,
97        start: Box<Materialized>,
98        len: Box<Materialized>,
99    },
100    StrEq(Box<Materialized>, Box<Materialized>),
101    StrContains {
102        haystack: Box<Materialized>,
103        needle: Box<Materialized>,
104    },
105    StrStartsWith {
106        s: Box<Materialized>,
107        prefix: Box<Materialized>,
108    },
109    StrIndexOf {
110        haystack: Box<Materialized>,
111        needle: Box<Materialized>,
112    },
113    NumberToStr(Box<Materialized>),
114    StrToNumber(Box<Materialized>),
115    StrToNumberOpt(Box<Materialized>),
116    Now,
117    List(Vec<Materialized>),
118    ListEmpty {
119        elem: Type,
120    },
121    ListCons {
122        head: Box<Materialized>,
123        tail: Box<Materialized>,
124    },
125    OptionSome(Box<Materialized>),
126    OptionNone {
127        elem: Type,
128    },
129    OptionElse {
130        opt: Box<Materialized>,
131        default: Box<Materialized>,
132    },
133    OptionMatch {
134        opt: Box<Materialized>,
135        some_bind: String,
136        some_body: Box<Materialized>,
137        none_body: Box<Materialized>,
138    },
139    ListTryGet {
140        list: Box<Materialized>,
141        index: Box<Materialized>,
142    },
143    ListLen(Box<Materialized>),
144    ListGet {
145        list: Box<Materialized>,
146        index: Box<Materialized>,
147    },
148    Map(Vec<(Materialized, Materialized)>),
149    MapGet {
150        map: Box<Materialized>,
151        key: Box<Materialized>,
152    },
153    MapTryGet {
154        map: Box<Materialized>,
155        key: Box<Materialized>,
156    },
157    MapLen(Box<Materialized>),
158    Log(Box<Materialized>),
159    Publish(Box<Materialized>),
160    SetHeader {
161        name: Box<Materialized>,
162        value: Box<Materialized>,
163    },
164    Rand,
165    MutNew(Box<Materialized>),
166    MutGet(Box<Materialized>),
167    MutSet {
168        cell: Box<Materialized>,
169        value: Box<Materialized>,
170    },
171    DiskWrite {
172        path: Box<Materialized>,
173        content: Box<Materialized>,
174    },
175    DiskRead(Box<Materialized>),
176    NetGet(Box<Materialized>),
177    DbQuery {
178        sql: Box<Materialized>,
179        params: Box<Materialized>,
180    },
181    Ref(String),
182    Call {
183        func: String,
184        args: Vec<Materialized>,
185    },
186    FuncRef(String),
187    CallValue {
188        callee: Box<Materialized>,
189        args: Vec<Materialized>,
190    },
191    Lambda {
192        params: Vec<Param>,
193        body: Box<Materialized>,
194    },
195    Hole {
196        expects: String,
197    },
198    Step {
199        binding: String,
200        value: Box<Materialized>,
201    },
202    BinOp {
203        op: BinOp,
204        lhs: Box<Materialized>,
205        rhs: Box<Materialized>,
206    },
207    If {
208        cond: Box<Materialized>,
209        then_branch: Box<Materialized>,
210        else_branch: Box<Materialized>,
211    },
212    Fail(String),
213    Handle {
214        body: Box<Materialized>,
215        handlers: Vec<(String, Materialized)>,
216    },
217    Function {
218        name: String,
219        type_params: Vec<String>,
220        params: Vec<Param>,
221        produces: Produces,
222        requires: BTreeSet<Effect>,
223        on_failure: Vec<String>,
224        body: Vec<Materialized>,
225        result: Box<Materialized>,
226    },
227    Module {
228        name: String,
229        types: Vec<Materialized>,
230        functions: Vec<Materialized>,
231    },
232    RecordDef {
233        name: String,
234        fields: Vec<(String, Type)>,
235    },
236    Record {
237        type_name: String,
238        fields: Vec<(String, Materialized)>,
239    },
240    Field {
241        base: Box<Materialized>,
242        type_name: String,
243        field: String,
244    },
245    VariantDef {
246        name: String,
247        cases: Vec<(String, Vec<(String, Type)>)>,
248    },
249    Variant {
250        type_name: String,
251        case: String,
252        fields: Vec<(String, Materialized)>,
253    },
254    Match {
255        scrutinee: Box<Materialized>,
256        type_name: String,
257        arms: Vec<(String, Vec<String>, Materialized)>,
258    },
259}
260
261/// The content-addressed store.
262/// The on-disk AST format version. The node model is content-addressed
263/// and serialized with `serde_json`; any change to a `Node` variant
264/// changes both the bytes and the hashes, so a store written by one
265/// version cannot be trusted by another. This integer is stamped into a
266/// new store and validated on open. Pre-1.0 there is no migration: a
267/// bump means existing stores are rebuilt, not upgraded — stated plainly
268/// rather than silently mis-decoding. Bump this on any `Node`/
269/// `canonical_bytes` change once releases exist.
270pub const FORMAT_VERSION: u32 = 1;
271
272pub struct Store {
273    conn: Connection,
274}
275
276impl Store {
277    /// Open or create a file-backed store.
278    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
279        Self::init(Connection::open(path)?)
280    }
281
282    /// Open an ephemeral in-memory store (used by tests).
283    pub fn open_in_memory() -> Result<Self> {
284        Self::init(Connection::open_in_memory()?)
285    }
286
287    fn init(conn: Connection) -> Result<Self> {
288        conn.execute_batch(
289            "CREATE TABLE IF NOT EXISTS nodes (
290                 hash TEXT PRIMARY KEY,
291                 body BLOB NOT NULL
292             );
293             CREATE TABLE IF NOT EXISTS refs (
294                 name TEXT PRIMARY KEY,
295                 root TEXT NOT NULL,
296                 kind TEXT NOT NULL CHECK (kind IN ('branch','checkpoint'))
297             );
298             CREATE TABLE IF NOT EXISTS meta (
299                 key TEXT PRIMARY KEY,
300                 value TEXT NOT NULL
301             );",
302        )?;
303        // Stamp a fresh store; validate an existing one. A store is
304        // "existing" iff it already holds nodes — an empty store (no
305        // nodes yet) is adopted at the current version regardless.
306        let recorded: Option<String> = conn
307            .query_row(
308                "SELECT value FROM meta WHERE key = 'format_version'",
309                [],
310                |row| row.get(0),
311            )
312            .optional()?;
313        match recorded {
314            Some(v) if v == FORMAT_VERSION.to_string() => {}
315            Some(v) => {
316                return Err(Error::FormatMismatch {
317                    found: v,
318                    expected: FORMAT_VERSION,
319                })
320            }
321            None => {
322                let has_nodes: bool = conn.query_row(
323                    "SELECT EXISTS(SELECT 1 FROM nodes LIMIT 1)",
324                    [],
325                    |row| row.get::<_, i64>(0),
326                )? != 0;
327                if has_nodes {
328                    // Nodes but no version marker = written before
329                    // versioning existed; its format is unknown.
330                    return Err(Error::FormatMismatch {
331                        found: "unversioned".into(),
332                        expected: FORMAT_VERSION,
333                    });
334                }
335                conn.execute(
336                    "INSERT INTO meta (key, value) VALUES ('format_version', ?1)",
337                    params![FORMAT_VERSION.to_string()],
338                )?;
339            }
340        }
341        Ok(Self { conn })
342    }
343
344    /// Store a node, returning its content hash. Idempotent: storing an
345    /// identical node again is a no-op, which is what dedups shared subtrees.
346    pub fn put(&self, node: &Node) -> Result<NodeHash> {
347        let hash = node.hash();
348        self.conn.execute(
349            "INSERT OR IGNORE INTO nodes (hash, body) VALUES (?1, ?2)",
350            params![hash.as_str(), node.canonical_bytes()],
351        )?;
352        Ok(hash)
353    }
354
355    /// Load a single node by hash.
356    pub fn get(&self, hash: &NodeHash) -> Result<Option<Node>> {
357        let body: Option<Vec<u8>> = self
358            .conn
359            .query_row(
360                "SELECT body FROM nodes WHERE hash = ?1",
361                params![hash.as_str()],
362                |row| row.get(0),
363            )
364            .optional()?;
365        match body {
366            Some(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
367            None => Ok(None),
368        }
369    }
370
371    /// Number of distinct nodes stored. Lets a test prove structural sharing.
372    pub fn node_count(&self) -> Result<i64> {
373        Ok(self
374            .conn
375            .query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))?)
376    }
377
378    /// Create an immutable checkpoint pointing at `root`. Errors if the name is
379    /// already taken by any ref.
380    pub fn checkpoint(&self, name: &str, root: &NodeHash) -> Result<()> {
381        if self.ref_kind(name)?.is_some() {
382            return Err(Error::NameInUse(name.to_string()));
383        }
384        self.conn.execute(
385            "INSERT INTO refs (name, root, kind) VALUES (?1, ?2, 'checkpoint')",
386            params![name, root.as_str()],
387        )?;
388        Ok(())
389    }
390
391    /// Create or move a branch to `root`. Refuses to overwrite a checkpoint.
392    pub fn branch(&self, name: &str, root: &NodeHash) -> Result<()> {
393        if self.ref_kind(name)?.as_deref() == Some("checkpoint") {
394            return Err(Error::NameInUse(name.to_string()));
395        }
396        self.conn.execute(
397            "INSERT INTO refs (name, root, kind) VALUES (?1, ?2, 'branch')
398             ON CONFLICT(name) DO UPDATE SET root = excluded.root",
399            params![name, root.as_str()],
400        )?;
401        Ok(())
402    }
403
404    /// Resolve a ref name to its root hash.
405    pub fn resolve(&self, name: &str) -> Result<Option<NodeHash>> {
406        let root: Option<String> = self
407            .conn
408            .query_row(
409                "SELECT root FROM refs WHERE name = ?1",
410                params![name],
411                |row| row.get(0),
412            )
413            .optional()?;
414        Ok(root.map(NodeHash::from_raw))
415    }
416
417    fn ref_kind(&self, name: &str) -> Result<Option<String>> {
418        Ok(self
419            .conn
420            .query_row(
421                "SELECT kind FROM refs WHERE name = ?1",
422                params![name],
423                |row| row.get(0),
424            )
425            .optional()?)
426    }
427
428    /// Recursively load the full subtree rooted at `hash`.
429    pub fn materialize(&self, hash: &NodeHash) -> Result<Materialized> {
430        let node = self
431            .get(hash)?
432            .ok_or_else(|| Error::MissingNode(hash.clone()))?;
433        Ok(match node {
434            Node::Lit(v) => Materialized::Lit(v),
435            Node::FloatLit(b) => Materialized::FloatLit(b),
436            Node::FloatOp { op, lhs, rhs } => Materialized::FloatOp {
437                op,
438                lhs: Box::new(self.materialize(&lhs)?),
439                rhs: Box::new(self.materialize(&rhs)?),
440            },
441            Node::IntToFloat(a) => {
442                Materialized::IntToFloat(Box::new(self.materialize(&a)?))
443            }
444            Node::FloatToInt(a) => {
445                Materialized::FloatToInt(Box::new(self.materialize(&a)?))
446            }
447            Node::DecimalLit(v) => Materialized::DecimalLit(v),
448            Node::DecimalOp { op, lhs, rhs } => Materialized::DecimalOp {
449                op,
450                lhs: Box::new(self.materialize(&lhs)?),
451                rhs: Box::new(self.materialize(&rhs)?),
452            },
453            Node::IntToDecimal(a) => {
454                Materialized::IntToDecimal(Box::new(self.materialize(&a)?))
455            }
456            Node::DecimalRaw(a) => {
457                Materialized::DecimalRaw(Box::new(self.materialize(&a)?))
458            }
459            Node::DecimalToInt(a) => {
460                Materialized::DecimalToInt(Box::new(self.materialize(&a)?))
461            }
462            Node::Bool(b) => Materialized::Bool(b),
463            Node::Not(a) => Materialized::Not(Box::new(self.materialize(&a)?)),
464            Node::Str(s) => Materialized::Str(s),
465            Node::StrLen(a) => Materialized::StrLen(Box::new(self.materialize(&a)?)),
466            Node::StrLower(a) => {
467                Materialized::StrLower(Box::new(self.materialize(&a)?))
468            }
469            Node::StrFromCode(a) => {
470                Materialized::StrFromCode(Box::new(self.materialize(&a)?))
471            }
472            Node::StrConcat(a, b) => Materialized::StrConcat(
473                Box::new(self.materialize(&a)?),
474                Box::new(self.materialize(&b)?),
475            ),
476            Node::StrSlice { s, start, len } => Materialized::StrSlice {
477                s: Box::new(self.materialize(&s)?),
478                start: Box::new(self.materialize(&start)?),
479                len: Box::new(self.materialize(&len)?),
480            },
481            Node::StrEq(a, b) => Materialized::StrEq(
482                Box::new(self.materialize(&a)?),
483                Box::new(self.materialize(&b)?),
484            ),
485            Node::StrContains { haystack, needle } => Materialized::StrContains {
486                haystack: Box::new(self.materialize(&haystack)?),
487                needle: Box::new(self.materialize(&needle)?),
488            },
489            Node::StrStartsWith { s, prefix } => Materialized::StrStartsWith {
490                s: Box::new(self.materialize(&s)?),
491                prefix: Box::new(self.materialize(&prefix)?),
492            },
493            Node::StrIndexOf { haystack, needle } => Materialized::StrIndexOf {
494                haystack: Box::new(self.materialize(&haystack)?),
495                needle: Box::new(self.materialize(&needle)?),
496            },
497            Node::NumberToStr(a) => {
498                Materialized::NumberToStr(Box::new(self.materialize(&a)?))
499            }
500            Node::StrToNumber(a) => {
501                Materialized::StrToNumber(Box::new(self.materialize(&a)?))
502            }
503            Node::StrToNumberOpt(a) => {
504                Materialized::StrToNumberOpt(Box::new(self.materialize(&a)?))
505            }
506            Node::Now => Materialized::Now,
507            Node::List(es) => {
508                let mut ms = Vec::with_capacity(es.len());
509                for e in es {
510                    ms.push(self.materialize(&e)?);
511                }
512                Materialized::List(ms)
513            }
514            Node::ListEmpty { elem } => Materialized::ListEmpty { elem },
515            Node::ListCons { head, tail } => Materialized::ListCons {
516                head: Box::new(self.materialize(&head)?),
517                tail: Box::new(self.materialize(&tail)?),
518            },
519            Node::OptionSome(v) => {
520                Materialized::OptionSome(Box::new(self.materialize(&v)?))
521            }
522            Node::OptionNone { elem } => Materialized::OptionNone { elem },
523            Node::OptionElse { opt, default } => Materialized::OptionElse {
524                opt: Box::new(self.materialize(&opt)?),
525                default: Box::new(self.materialize(&default)?),
526            },
527            Node::OptionMatch {
528                opt,
529                some_bind,
530                some_body,
531                none_body,
532            } => Materialized::OptionMatch {
533                opt: Box::new(self.materialize(&opt)?),
534                some_bind,
535                some_body: Box::new(self.materialize(&some_body)?),
536                none_body: Box::new(self.materialize(&none_body)?),
537            },
538            Node::ListTryGet { list, index } => Materialized::ListTryGet {
539                list: Box::new(self.materialize(&list)?),
540                index: Box::new(self.materialize(&index)?),
541            },
542            Node::ListLen(a) => Materialized::ListLen(Box::new(self.materialize(&a)?)),
543            Node::ListGet { list, index } => Materialized::ListGet {
544                list: Box::new(self.materialize(&list)?),
545                index: Box::new(self.materialize(&index)?),
546            },
547            Node::Map(pairs) => {
548                let mut ms = Vec::with_capacity(pairs.len());
549                for (k, v) in pairs {
550                    ms.push((self.materialize(&k)?, self.materialize(&v)?));
551                }
552                Materialized::Map(ms)
553            }
554            Node::MapGet { map, key } => Materialized::MapGet {
555                map: Box::new(self.materialize(&map)?),
556                key: Box::new(self.materialize(&key)?),
557            },
558            Node::MapTryGet { map, key } => Materialized::MapTryGet {
559                map: Box::new(self.materialize(&map)?),
560                key: Box::new(self.materialize(&key)?),
561            },
562            Node::MapLen(a) => Materialized::MapLen(Box::new(self.materialize(&a)?)),
563            Node::Log(a) => Materialized::Log(Box::new(self.materialize(&a)?)),
564            Node::Publish(a) => {
565                Materialized::Publish(Box::new(self.materialize(&a)?))
566            }
567            Node::SetHeader { name, value } => Materialized::SetHeader {
568                name: Box::new(self.materialize(&name)?),
569                value: Box::new(self.materialize(&value)?),
570            },
571            Node::Rand => Materialized::Rand,
572            Node::MutNew(v) => Materialized::MutNew(Box::new(self.materialize(&v)?)),
573            Node::MutGet(c) => Materialized::MutGet(Box::new(self.materialize(&c)?)),
574            Node::MutSet { cell, value } => Materialized::MutSet {
575                cell: Box::new(self.materialize(&cell)?),
576                value: Box::new(self.materialize(&value)?),
577            },
578            Node::DiskWrite { path, content } => Materialized::DiskWrite {
579                path: Box::new(self.materialize(&path)?),
580                content: Box::new(self.materialize(&content)?),
581            },
582            Node::DiskRead(p) => {
583                Materialized::DiskRead(Box::new(self.materialize(&p)?))
584            }
585            Node::NetGet(u) => {
586                Materialized::NetGet(Box::new(self.materialize(&u)?))
587            }
588            Node::DbQuery { sql, params } => Materialized::DbQuery {
589                sql: Box::new(self.materialize(&sql)?),
590                params: Box::new(self.materialize(&params)?),
591            },
592            Node::Ref(name) => Materialized::Ref(name),
593            Node::Call { func, args } => Materialized::Call {
594                func,
595                args: args
596                    .iter()
597                    .map(|h| self.materialize(h))
598                    .collect::<Result<_>>()?,
599            },
600            Node::FuncRef(name) => Materialized::FuncRef(name),
601            Node::CallValue { callee, args } => Materialized::CallValue {
602                callee: Box::new(self.materialize(&callee)?),
603                args: args
604                    .iter()
605                    .map(|h| self.materialize(h))
606                    .collect::<Result<_>>()?,
607            },
608            Node::Lambda { params, body } => Materialized::Lambda {
609                params,
610                body: Box::new(self.materialize(&body)?),
611            },
612            Node::Hole { expects } => Materialized::Hole { expects },
613            Node::Step { binding, value } => Materialized::Step {
614                binding,
615                value: Box::new(self.materialize(&value)?),
616            },
617            Node::BinOp { op, lhs, rhs } => Materialized::BinOp {
618                op,
619                lhs: Box::new(self.materialize(&lhs)?),
620                rhs: Box::new(self.materialize(&rhs)?),
621            },
622            Node::If {
623                cond,
624                then_branch,
625                else_branch,
626            } => Materialized::If {
627                cond: Box::new(self.materialize(&cond)?),
628                then_branch: Box::new(self.materialize(&then_branch)?),
629                else_branch: Box::new(self.materialize(&else_branch)?),
630            },
631            Node::Fail(v) => Materialized::Fail(v),
632            Node::Handle { body, handlers } => {
633                let mut hs = Vec::with_capacity(handlers.len());
634                for (variant, recover) in handlers {
635                    hs.push((variant, self.materialize(&recover)?));
636                }
637                Materialized::Handle {
638                    body: Box::new(self.materialize(&body)?),
639                    handlers: hs,
640                }
641            }
642            Node::Function {
643                name,
644                type_params,
645                params,
646                produces,
647                requires,
648                on_failure,
649                body,
650                result,
651            } => Materialized::Function {
652                name,
653                type_params,
654                params,
655                produces,
656                requires,
657                on_failure,
658                body: body
659                    .iter()
660                    .map(|h| self.materialize(h))
661                    .collect::<Result<_>>()?,
662                result: Box::new(self.materialize(&result)?),
663            },
664            Node::Module {
665                name,
666                types,
667                functions,
668            } => Materialized::Module {
669                name,
670                types: types
671                    .iter()
672                    .map(|h| self.materialize(h))
673                    .collect::<Result<_>>()?,
674                functions: functions
675                    .iter()
676                    .map(|h| self.materialize(h))
677                    .collect::<Result<_>>()?,
678            },
679            Node::RecordDef { name, fields } => Materialized::RecordDef { name, fields },
680            Node::Record { type_name, fields } => {
681                let mut fs = Vec::with_capacity(fields.len());
682                for (n, h) in fields {
683                    fs.push((n, self.materialize(&h)?));
684                }
685                Materialized::Record {
686                    type_name,
687                    fields: fs,
688                }
689            }
690            Node::Field {
691                base,
692                type_name,
693                field,
694            } => Materialized::Field {
695                base: Box::new(self.materialize(&base)?),
696                type_name,
697                field,
698            },
699            Node::VariantDef { name, cases } => Materialized::VariantDef { name, cases },
700            Node::Variant {
701                type_name,
702                case,
703                fields,
704            } => {
705                let mut fs = Vec::with_capacity(fields.len());
706                for (n, h) in fields {
707                    fs.push((n, self.materialize(&h)?));
708                }
709                Materialized::Variant {
710                    type_name,
711                    case,
712                    fields: fs,
713                }
714            }
715            Node::Match {
716                scrutinee,
717                type_name,
718                arms,
719            } => {
720                let mut ms = Vec::with_capacity(arms.len());
721                for a in arms {
722                    ms.push((a.case, a.bindings, self.materialize(&a.body)?));
723                }
724                Materialized::Match {
725                    scrutinee: Box::new(self.materialize(&scrutinee)?),
726                    type_name,
727                    arms: ms,
728                }
729            }
730        })
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::node::{Param, Produces};
738    use crate::ty::{Confidence, Type};
739    use std::collections::BTreeSet;
740
741    /// Build the same small function tree and return its root hash. The tree:
742    ///
743    ///   function double(n) { step doubled = add(n, n); result = doubled }
744    ///
745    /// `add(n, n)` reuses the identical `Ref("n")` node twice on purpose, to
746    /// exercise structural sharing.
747    fn build(store: &Store) -> NodeHash {
748        let n = store.put(&Node::Ref("n".into())).unwrap();
749        let call = store
750            .put(&Node::Call {
751                func: "add".into(),
752                args: vec![n.clone(), n.clone()],
753            })
754            .unwrap();
755        let step = store
756            .put(&Node::Step {
757                binding: "doubled".into(),
758                value: call,
759            })
760            .unwrap();
761        let result = store.put(&Node::Ref("doubled".into())).unwrap();
762        store
763            .put(&Node::Function {
764                name: "double".into(),
765                type_params: vec![],
766                params: vec![Param {
767                    name: "n".into(),
768                    ty: Type::Number,
769                    min_confidence: Confidence::External,
770                }],
771                produces: Produces {
772                    ty: Type::Number,
773                    confidence: Confidence::Structural,
774                },
775                requires: BTreeSet::new(),
776                on_failure: vec![],
777                body: vec![step],
778                result,
779            })
780            .unwrap()
781    }
782
783    #[test]
784    fn hashing_is_deterministic_and_merkle() {
785        let a = Store::open_in_memory().unwrap();
786        let b = Store::open_in_memory().unwrap();
787        // Same logical tree, two independent stores → identical root hash.
788        assert_eq!(build(&a), build(&b));
789    }
790
791    #[test]
792    fn identical_subtrees_are_stored_once() {
793        let s = Store::open_in_memory().unwrap();
794        build(&s);
795        // Ref("n") appears twice but is one node; Ref("doubled") is distinct.
796        // Distinct nodes: Ref("n"), Call, Step, Ref("doubled"), Function = 5.
797        assert_eq!(s.node_count().unwrap(), 5);
798    }
799
800    #[test]
801    fn checkpoint_round_trips_exactly() {
802        let s = Store::open_in_memory().unwrap();
803        let root = build(&s);
804        let before = s.materialize(&root).unwrap();
805
806        s.checkpoint("v1", &root).unwrap();
807        let resolved = s.resolve("v1").unwrap().expect("v1 resolves");
808        assert_eq!(resolved, root);
809        assert_eq!(s.materialize(&resolved).unwrap(), before);
810    }
811
812    #[test]
813    fn checkpoints_are_immutable() {
814        let s = Store::open_in_memory().unwrap();
815        let root = build(&s);
816        s.checkpoint("v1", &root).unwrap();
817        assert!(matches!(
818            s.checkpoint("v1", &root),
819            Err(Error::NameInUse(_))
820        ));
821    }
822
823    #[test]
824    fn branches_move_but_cannot_clobber_a_checkpoint() {
825        let s = Store::open_in_memory().unwrap();
826        let root = build(&s);
827        let leaf = s.put(&Node::Lit(1)).unwrap();
828
829        s.branch("main", &root).unwrap();
830        assert_eq!(s.resolve("main").unwrap().unwrap(), root);
831        s.branch("main", &leaf).unwrap();
832        assert_eq!(s.resolve("main").unwrap().unwrap(), leaf);
833
834        s.checkpoint("release", &root).unwrap();
835        assert!(matches!(
836            s.branch("release", &leaf),
837            Err(Error::NameInUse(_))
838        ));
839    }
840
841    #[test]
842    fn persists_across_reopen() {
843        let mut path = std::env::temp_dir();
844        path.push(format!("cairn-store-test-{}.sqlite", std::process::id()));
845        let _ = std::fs::remove_file(&path);
846
847        let root = {
848            let s = Store::open(&path).unwrap();
849            let root = build(&s);
850            s.checkpoint("v1", &root).unwrap();
851            root
852        };
853        {
854            let s = Store::open(&path).unwrap();
855            let resolved = s.resolve("v1").unwrap().expect("v1 survives reopen");
856            assert_eq!(resolved, root);
857            assert_eq!(
858                s.materialize(&resolved).unwrap(),
859                Store::open_in_memory()
860                    .map(|m| m.materialize(&build(&m)).unwrap())
861                    .unwrap()
862            );
863        }
864        std::fs::remove_file(&path).unwrap();
865    }
866
867    #[test]
868    fn refuses_a_foreign_or_unversioned_store() {
869        let mut path = std::env::temp_dir();
870        path.push(format!("cairn-fmt-test-{}.sqlite", std::process::id()));
871        let _ = std::fs::remove_file(&path);
872
873        // A normally-created store carries the current version and
874        // reopens cleanly.
875        {
876            let s = Store::open(&path).unwrap();
877            s.put(&Node::Lit(1)).unwrap();
878        }
879        assert!(Store::open(&path).is_ok(), "same-version store must reopen");
880
881        // Tamper the recorded version → opening is refused, not
882        // silently mis-decoded.
883        {
884            let c = Connection::open(&path).unwrap();
885            c.execute(
886                "UPDATE meta SET value = '999' WHERE key = 'format_version'",
887                [],
888            )
889            .unwrap();
890        }
891        match Store::open(&path) {
892            Err(Error::FormatMismatch { found, expected }) => {
893                assert_eq!(found, "999");
894                assert_eq!(expected, FORMAT_VERSION);
895            }
896            Err(e) => panic!("expected FormatMismatch, got {e:?}"),
897            Ok(_) => panic!("expected FormatMismatch, store opened"),
898        }
899        std::fs::remove_file(&path).unwrap();
900
901        // A store with nodes but no version marker (written before
902        // versioning existed) is also refused rather than adopted.
903        path.push("");
904        path.set_file_name(format!("cairn-fmt-old-{}.sqlite", std::process::id()));
905        let _ = std::fs::remove_file(&path);
906        {
907            let c = Connection::open(&path).unwrap();
908            c.execute_batch(
909                "CREATE TABLE nodes (hash TEXT PRIMARY KEY, body BLOB NOT NULL);
910                 INSERT INTO nodes (hash, body) VALUES ('h', x'00');",
911            )
912            .unwrap();
913        }
914        assert!(
915            matches!(Store::open(&path), Err(Error::FormatMismatch { .. })),
916            "an unversioned store holding nodes must be refused"
917        );
918        std::fs::remove_file(&path).unwrap();
919    }
920}