1use 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#[derive(Debug)]
19pub enum Error {
20 Sqlite(rusqlite::Error),
21 Decode(serde_json::Error),
22 NameInUse(String),
25 MissingNode(NodeHash),
27 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#[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
261pub const FORMAT_VERSION: u32 = 1;
271
272pub struct Store {
273 conn: Connection,
274}
275
276impl Store {
277 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
279 Self::init(Connection::open(path)?)
280 }
281
282 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 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 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 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 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 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 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 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 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 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(¶ms)?),
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 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 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 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 {
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 {
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 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}