1use libxml::tree::Node;
19use rustc_hash::FxHashMap as HashMap;
20
21#[derive(Debug, Clone)]
25pub struct Entry {
26 pub key: String,
28 values: HashMap<String, Value>,
30}
31
32#[derive(Debug, Clone)]
36pub enum Value {
37 String(String),
39 Int(i64),
41 Bool(bool),
43 List(Vec<Value>),
45 Hash(HashMap<String, Value>),
47 Xml(Node),
49 Null,
51}
52
53impl Value {
54 pub fn as_str(&self) -> Option<&str> {
56 match self {
57 Value::String(s) => Some(s),
58 _ => None,
59 }
60 }
61
62 pub fn as_string(&self) -> String {
64 match self {
65 Value::String(s) => s.clone(),
66 Value::Int(n) => n.to_string(),
67 Value::Bool(b) => b.to_string(),
68 Value::Xml(node) => node.get_content(),
69 Value::Null => String::new(),
70 _ => String::new(),
71 }
72 }
73
74 pub fn is_truthy(&self) -> bool {
76 match self {
77 Value::Null => false,
78 Value::String(s) => !s.is_empty(),
79 Value::Bool(b) => *b,
80 Value::List(v) => !v.is_empty(),
81 Value::Hash(h) => !h.is_empty(),
82 _ => true,
83 }
84 }
85}
86
87impl From<&str> for Value {
88 fn from(s: &str) -> Self { Value::String(s.to_string()) }
89}
90
91impl From<String> for Value {
92 fn from(s: String) -> Self { Value::String(s) }
93}
94
95impl std::fmt::Display for Value {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 Value::String(s) => write!(f, "{}", s),
99 Value::Int(n) => write!(f, "{}", n),
100 Value::Bool(b) => write!(f, "{}", b),
101 Value::Null => Ok(()),
102 Value::Xml(node) => write!(f, "{}", node.get_content()),
103 Value::List(_) | Value::Hash(_) => Ok(()),
104 }
105 }
106}
107
108impl From<bool> for Value {
115 fn from(b: bool) -> Self { Value::Bool(b) }
116}
117
118impl From<Vec<String>> for Value {
119 fn from(v: Vec<String>) -> Self { Value::List(v.into_iter().map(Value::String).collect()) }
120}
121
122impl Entry {
123 pub fn new(key: &str) -> Self {
125 Entry {
126 key: key.to_string(),
127 values: HashMap::default(),
128 }
129 }
130
131 pub fn get_key(&self) -> &str { &self.key }
133
134 pub fn has_value(&self, attr: &str) -> bool { self.values.contains_key(attr) }
136
137 pub fn get_value(&self, attr: &str) -> Option<&Value> { self.values.get(attr) }
139
140 pub fn get_string(&self, attr: &str) -> Option<&str> {
142 self.values.get(attr).and_then(|v| v.as_str())
143 }
144
145 pub fn get_xml(&self, attr: &str) -> Option<&Node> {
147 match self.values.get(attr) {
148 Some(Value::Xml(n)) => Some(n),
149 _ => None,
150 }
151 }
152
153 pub fn get_children(&self) -> Vec<String> {
155 match self.values.get("children") {
156 Some(Value::List(items)) => items
157 .iter()
158 .filter_map(|v| v.as_str().map(String::from))
159 .collect(),
160 _ => vec![],
161 }
162 }
163
164 pub fn set_values(&mut self, pairs: Vec<(&str, Value)>) {
168 for (key, value) in pairs {
169 match value {
170 Value::Null => {
171 self.values.remove(key);
172 },
173 _ => {
174 self.values.insert(key.to_string(), value);
175 },
176 }
177 }
178 }
179
180 pub fn set_value(&mut self, attr: &str, value: Value) {
182 match value {
183 Value::Null => {
184 self.values.remove(attr);
185 },
186 _ => {
187 self.values.insert(attr.to_string(), value);
188 },
189 }
190 }
191
192 pub fn push_values(&mut self, attr: &str, values: Vec<Value>) {
196 let list = self
197 .values
198 .entry(attr.to_string())
199 .or_insert_with(|| Value::List(Vec::new()));
200 if let Value::List(items) = list {
201 for v in values {
202 items.push(v);
203 }
204 }
205 }
206
207 pub fn push_new(&mut self, attr: &str, values: Vec<Value>) {
211 let list = self
212 .values
213 .entry(attr.to_string())
214 .or_insert_with(|| Value::List(Vec::new()));
215 if let Value::List(items) = list {
216 for v in values {
217 let s = v.to_string();
218 if !items.iter().any(|existing| existing.to_string() == s) {
219 items.push(v);
220 }
221 }
222 }
223 }
224
225 pub fn note_association(&mut self, keys: &[&str]) {
230 if keys.is_empty() {
231 return;
232 }
233 if keys.len() == 1 {
234 self.values.insert(keys[0].to_string(), Value::Bool(true));
235 return;
236 }
237
238 let first = keys[0];
240 let rest = &keys[1..];
241
242 let hash = self
243 .values
244 .entry(first.to_string())
245 .or_insert_with(|| Value::Hash(HashMap::default()));
246
247 if let Value::Hash(h) = hash {
248 let mut current = h;
249 for (i, &key) in rest.iter().enumerate() {
250 if i == rest.len() - 1 {
251 current.insert(key.to_string(), Value::Bool(true));
253 } else {
254 let entry = current
256 .entry(key.to_string())
257 .or_insert_with(|| Value::Hash(HashMap::default()));
258 if let Value::Hash(inner) = entry {
259 current = inner;
260 } else {
261 break;
262 }
263 }
264 }
265 }
266 }
267}
268
269pub struct ObjectDB {
275 objects: HashMap<String, Entry>,
277 xml_holder: Option<libxml::tree::Document>,
294 external: Option<ExternalDb>,
297}
298
299impl ObjectDB {
300 pub fn new() -> Self {
302 ObjectDB {
303 objects: HashMap::default(),
304 xml_holder: None,
305 external: None,
306 }
307 }
308
309 fn xml_holder_mut(&mut self) -> Option<&mut libxml::tree::Document> {
314 if self.xml_holder.is_none() {
315 let mut doc = libxml::tree::Document::new().ok()?;
316 let root = Node::new("_objectdb_", None, &doc).ok()?;
317 doc.set_root_element(&root);
318 self.xml_holder = Some(doc);
319 }
320 self.xml_holder.as_mut()
321 }
322
323 pub fn adopt_xml(&mut self, node: &Node) -> Option<Value> {
342 let scratch = libxml::tree::Document::dup_node_into_new_doc(node).ok()?;
343 let mut extracted = scratch.get_root_element()?;
344 extracted.unlink_node();
345 let holder = self.xml_holder_mut()?;
346 let mut adopted = holder.import_node(&mut extracted).ok()?;
347 let mut root = holder.get_root_element()?;
348 root.add_child(&mut adopted).ok()?;
349 extracted.free_subtree();
350 Some(Value::Xml(adopted))
351 }
352
353 pub fn lookup(&self, key: &str) -> Option<&Entry> { self.objects.get(key) }
357
358 pub fn lookup_mut(&mut self, key: &str) -> Option<&mut Entry> { self.objects.get_mut(key) }
360
361 pub fn register(&mut self, key: &str, props: Vec<(&str, Value)>) -> &mut Entry {
366 let entry = self
367 .objects
368 .entry(key.to_string())
369 .or_insert_with(|| Entry::new(key));
370 if !props.is_empty() {
371 entry.set_values(props);
372 }
373 self.objects.get_mut(key).unwrap()
374 }
375
376 pub fn unregister(&mut self, key: &str) {
380 self.objects.remove(key);
381 if let Some(external) = &mut self.external {
385 external.baseline.remove(key);
386 if !external.readonly {
387 let _ = external
388 .conn
389 .execute("DELETE FROM entries WHERE key = ?1", rusqlite::params![key]);
390 }
391 }
392 }
393
394 pub fn get_keys(&self) -> Vec<&String> {
398 let mut keys: Vec<_> = self.objects.keys().collect();
399 keys.sort();
400 keys
401 }
402
403 pub fn len(&self) -> usize { self.objects.len() }
406
407 pub fn is_empty(&self) -> bool { self.objects.is_empty() }
409
410 pub fn keys_iter(&self) -> impl Iterator<Item = &String> { self.objects.keys() }
413
414 pub fn status(&self) -> String { format!("{} objects", self.objects.len()) }
418}
419
420impl Default for ObjectDB {
421 fn default() -> Self { Self::new() }
422}
423
424pub const DBFILE_FORMAT_VERSION: i64 = 1;
449
450struct ExternalDb {
454 conn: rusqlite::Connection,
455 readonly: bool,
456 baseline: HashMap<String, String>,
457}
458
459#[derive(Default)]
462pub struct DbAttachOptions {
463 pub clean: bool,
465 pub readonly: bool,
467}
468
469impl ObjectDB {
470 pub fn attach(dbfile: &std::path::Path, options: DbAttachOptions) -> Result<Self, String> {
473 if options.clean && options.readonly {
474 return Err("dbfile: clean requires write access (Perl always opens O_RDWR)".to_string());
475 }
476 if options.clean && dbfile.exists() {
477 latexml_core::common::error::emit_warn(
478 "expected",
479 "dbfile",
480 &format!("Removing Object database file {}!", dbfile.display()),
481 );
482 std::fs::remove_file(dbfile)
483 .map_err(|e| format!("cannot remove {}: {e}", dbfile.display()))?;
484 }
485 let conn = if options.readonly {
486 rusqlite::Connection::open_with_flags(dbfile, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
487 } else {
488 rusqlite::Connection::open(dbfile)
489 }
490 .map_err(|e| format!("cannot attach DB {}: {e}", dbfile.display()))?;
491 if !options.readonly {
495 let _ = conn.query_row("PRAGMA journal_mode = WAL", [], |_| Ok(()));
496 }
497 conn
498 .busy_timeout(std::time::Duration::from_secs(10))
499 .map_err(|e| format!("cannot set busy_timeout: {e}"))?;
500
501 let version: i64 = conn
502 .query_row("PRAGMA user_version", [], |r| r.get(0))
503 .map_err(|e| format!("cannot read user_version: {e}"))?;
504 let entries_table: bool = conn
505 .query_row(
506 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='entries'",
507 [],
508 |r| r.get::<_, i64>(0),
509 )
510 .map(|n| n > 0)
511 .unwrap_or(false);
512 let any_table: bool = conn
513 .query_row(
514 "SELECT COUNT(*) FROM sqlite_master WHERE type='table'",
515 [],
516 |r| r.get::<_, i64>(0),
517 )
518 .map(|n| n > 0)
519 .unwrap_or(false);
520 if version == 0 && any_table && !entries_table {
521 return Err(format!(
522 "{} is a SQLite file but not an ObjectDB (no entries table) — refusing to adopt it",
523 dbfile.display()
524 ));
525 }
526 if version == 0 && !options.readonly {
527 conn
528 .execute_batch(&format!(
529 "PRAGMA user_version = {DBFILE_FORMAT_VERSION};
530 CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
531 CREATE TABLE IF NOT EXISTS entries(key TEXT PRIMARY KEY, props TEXT NOT NULL);"
532 ))
533 .map_err(|e| format!("cannot initialize {}: {e}", dbfile.display()))?;
534 } else if version != 0 && version != DBFILE_FORMAT_VERSION {
535 return Err(format!(
536 "object database {} has format version {version}, this binary reads {DBFILE_FORMAT_VERSION} — rebuild it (--dbfile with a clean run)",
537 dbfile.display()
538 ));
539 }
540
541 let mut db = ObjectDB::new();
542 let mut baseline = HashMap::default();
543 let table_exists: bool = conn
545 .query_row(
546 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='entries'",
547 [],
548 |r| r.get::<_, i64>(0),
549 )
550 .map(|n| n > 0)
551 .unwrap_or(false);
552 if table_exists {
553 let mut stmt = conn
554 .prepare("SELECT key, props FROM entries")
555 .map_err(|e| format!("cannot read entries: {e}"))?;
556 let rows = stmt
557 .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
558 .map_err(|e| format!("cannot scan entries: {e}"))?;
559 for row in rows {
560 let (key, props) = row.map_err(|e| format!("bad row: {e}"))?;
561 match db.decode_entry(&key, &props) {
564 Ok(entry) => {
565 db.objects.insert(key.clone(), entry);
566 baseline.insert(key, props);
567 },
568 Err(e) => latexml_core::common::error::emit_warn("malformed", "dbfile_entry", &e),
569 }
570 }
571 drop(stmt);
572 }
573 db.external = Some(ExternalDb {
574 conn,
575 readonly: options.readonly,
576 baseline,
577 });
578 Ok(db)
579 }
580
581 pub fn save_as(&self, path: &std::path::Path) -> Result<usize, String> {
587 if path.exists() {
588 std::fs::remove_file(path).map_err(|e| format!("cannot replace {}: {e}", path.display()))?;
589 }
590 let mut conn = rusqlite::Connection::open(path)
591 .map_err(|e| format!("cannot create dbfile {}: {e}", path.display()))?;
592 let _ = conn.query_row("PRAGMA journal_mode = WAL", [], |_| Ok(()));
593 conn
594 .execute_batch(&format!(
595 "PRAGMA user_version = {DBFILE_FORMAT_VERSION};
596 CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
597 CREATE TABLE entries(key TEXT PRIMARY KEY, props TEXT NOT NULL);"
598 ))
599 .map_err(|e| format!("cannot initialize {}: {e}", path.display()))?;
600 let tx = conn
601 .transaction()
602 .map_err(|e| format!("cannot begin save_as transaction: {e}"))?;
603 let mut stored = 0usize;
604 for (key, entry) in &self.objects {
605 let map: serde_json::Map<String, serde_json::Value> = entry
606 .values
607 .iter()
608 .map(|(k, v)| (k.clone(), value_to_json_with(self.xml_holder.as_ref(), v)))
609 .collect();
610 tx.execute(
611 "INSERT INTO entries(key, props) VALUES (?1, ?2)",
612 rusqlite::params![key, serde_json::Value::Object(map).to_string()],
613 )
614 .map_err(|e| format!("cannot store entry {key}: {e}"))?;
615 stored += 1;
616 }
617 tx.execute(
618 "INSERT INTO meta(key, value) VALUES ('latexml_version', ?1)",
619 rusqlite::params![env!("CARGO_PKG_VERSION")],
620 )
621 .map_err(|e| format!("cannot store meta: {e}"))?;
622 tx.commit()
623 .map_err(|e| format!("cannot commit save_as: {e}"))?;
624 Ok(stored)
625 }
626
627 pub fn finish(&mut self) -> Result<usize, String> {
631 let Some(external) = self.external.take() else {
632 return Ok(0);
633 };
634 if external.readonly {
635 return Ok(0);
636 }
637 let mut stored = 0usize;
642 let mut conn = external.conn;
643 let tx = conn
644 .transaction()
645 .map_err(|e| format!("cannot begin dbfile transaction: {e}"))?;
646 for (key, entry) in &self.objects {
647 let props = {
648 let map: serde_json::Map<String, serde_json::Value> = entry
649 .values
650 .iter()
651 .map(|(k, v)| (k.clone(), value_to_json_with(self.xml_holder.as_ref(), v)))
652 .collect();
653 serde_json::Value::Object(map).to_string()
654 };
655 if external.baseline.get(key) == Some(&props) {
656 continue;
657 }
658 tx.execute(
659 "INSERT INTO entries(key, props) VALUES (?1, ?2)
660 ON CONFLICT(key) DO UPDATE SET props = excluded.props",
661 rusqlite::params![key, props],
662 )
663 .map_err(|e| format!("cannot store entry {key}: {e}"))?;
664 stored += 1;
665 }
666 tx.execute(
667 "INSERT INTO meta(key, value) VALUES ('latexml_version', ?1)
668 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
669 rusqlite::params![env!("CARGO_PKG_VERSION")],
670 )
671 .map_err(|e| format!("cannot store meta: {e}"))?;
672 tx.commit()
673 .map_err(|e| format!("cannot commit dbfile: {e}"))?;
674 Ok(stored)
675 }
676
677 fn value_to_json(&self, value: &Value) -> serde_json::Value {
682 value_to_json_with(self.xml_holder.as_ref(), value)
683 }
684
685 fn decode_entry(&mut self, key: &str, props: &str) -> Result<Entry, String> {
686 let parsed: serde_json::Value =
687 serde_json::from_str(props).map_err(|e| format!("entry {key} is not valid JSON: {e}"))?;
688 let serde_json::Value::Object(map) = parsed else {
689 return Err(format!("entry {key} is not a JSON object"));
690 };
691 let mut entry = Entry::new(key);
692 for (attr, jv) in map {
693 let value = self.json_to_value(&jv, key)?;
694 entry.set_value(&attr, value);
695 }
696 Ok(entry)
697 }
698
699 fn json_to_value(&mut self, jv: &serde_json::Value, key: &str) -> Result<Value, String> {
700 use serde_json::Value as J;
701 let J::Object(o) = jv else {
702 return match jv {
703 J::Null => Ok(Value::Null),
704 other => Err(format!("entry {key}: unexpected bare JSON value {other}")),
705 };
706 };
707 if let Some((tag, inner)) = o.iter().next()
708 && o.len() == 1
709 {
710 return match (tag.as_str(), inner) {
711 ("s", J::String(s)) => Ok(Value::String(s.clone())),
712 ("i", J::Number(n)) => n
713 .as_i64()
714 .map(Value::Int)
715 .ok_or_else(|| format!("entry {key}: non-integer numeric value {n}")),
716 ("b", J::Bool(b)) => Ok(Value::Bool(*b)),
717 ("l", J::Array(items)) => Ok(Value::List(
718 items
719 .iter()
720 .map(|v| self.json_to_value(v, key))
721 .collect::<Result<Vec<_>, _>>()?,
722 )),
723 ("h", J::Object(map)) => {
724 let mut out = HashMap::default();
725 for (k, v) in map {
726 out.insert(k.clone(), self.json_to_value(v, key)?);
727 }
728 Ok(Value::Hash(out))
729 },
730 ("x", J::String(xml)) => {
731 let parsed = libxml::parser::Parser::default()
732 .parse_string(xml)
733 .map_err(|e| format!("entry {key}: stored XML does not parse: {e}"))?;
734 let root = parsed
735 .get_root_element()
736 .ok_or_else(|| format!("entry {key}: stored XML is empty"))?;
737 self
738 .adopt_xml(&root)
739 .ok_or_else(|| format!("entry {key}: could not adopt stored XML"))
740 },
741 (tag, _) => Err(format!("entry {key}: unknown value tag '{tag}'")),
742 };
743 }
744 Err(format!("entry {key}: malformed value object"))
745 }
746}
747
748fn value_to_json_with(holder: Option<&libxml::tree::Document>, value: &Value) -> serde_json::Value {
751 use serde_json::{Value as J, json};
752 match value {
753 Value::String(s) => json!({ "s": s }),
754 Value::Int(i) => json!({ "i": i }),
755 Value::Bool(b) => json!({ "b": b }),
756 Value::Null => J::Null,
757 Value::List(items) => {
758 json!({ "l": items.iter().map(|v| value_to_json_with(holder, v)).collect::<Vec<_>>() })
759 },
760 Value::Hash(map) => {
761 json!({ "h": map.iter().map(|(k, v)| (k.clone(), value_to_json_with(holder, v))).collect::<serde_json::Map<_, _>>() })
762 },
763 Value::Xml(node) => {
764 let xml = holder
765 .map(|doc| doc.node_to_string(node))
766 .unwrap_or_default();
767 json!({ "x": xml })
768 },
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 #[test]
777 fn test_entry_basic() {
778 let mut entry = Entry::new("test:key");
779 assert_eq!(entry.get_key(), "test:key");
780 assert!(!entry.has_value("name"));
781
782 entry.set_value("name", Value::from("Alice"));
783 assert!(entry.has_value("name"));
784 assert_eq!(entry.get_string("name"), Some("Alice"));
785 }
786
787 #[test]
788 fn test_entry_push_new() {
789 let mut entry = Entry::new("test");
790 entry.push_new("children", vec![Value::from("a"), Value::from("b")]);
791 entry.push_new("children", vec![Value::from("b"), Value::from("c")]);
792 let children = entry.get_children();
794 assert_eq!(children, vec!["a", "b", "c"]);
795 }
796
797 #[test]
798 fn test_entry_note_association() {
799 let mut entry = Entry::new("test");
800 entry.note_association(&["referrers", "doc1"]);
801 entry.note_association(&["referrers", "doc2"]);
802
803 assert!(entry.has_value("referrers"));
804 if let Some(Value::Hash(refs)) = entry.get_value("referrers") {
805 assert!(refs.contains_key("doc1"));
806 assert!(refs.contains_key("doc2"));
807 } else {
808 panic!("Expected Hash value for referrers");
809 }
810 }
811
812 #[test]
813 fn test_db_register_lookup() {
814 let mut db = ObjectDB::new();
815
816 db.register("ID:doc1", vec![
817 ("type", Value::from("ltx:document")),
818 ("title", Value::from("Test Document")),
819 ]);
820
821 let entry = db.lookup("ID:doc1");
822 assert!(entry.is_some());
823 assert_eq!(entry.unwrap().get_string("type"), Some("ltx:document"));
824 assert_eq!(entry.unwrap().get_string("title"), Some("Test Document"));
825
826 assert!(db.lookup("ID:missing").is_none());
828 }
829
830 #[test]
831 fn test_db_register_updates() {
832 let mut db = ObjectDB::new();
833 db.register("ID:x", vec![("a", Value::from("1"))]);
834 db.register("ID:x", vec![("b", Value::from("2"))]);
835
836 let entry = db.lookup("ID:x").unwrap();
837 assert_eq!(entry.get_string("a"), Some("1"));
838 assert_eq!(entry.get_string("b"), Some("2"));
839 }
840
841 #[test]
842 fn test_db_get_keys() {
843 let mut db = ObjectDB::new();
844 db.register("B", vec![]);
845 db.register("A", vec![]);
846 db.register("C", vec![]);
847
848 let keys = db.get_keys();
849 assert_eq!(keys, vec![
850 &"A".to_string(),
851 &"B".to_string(),
852 &"C".to_string()
853 ]);
854 }
855
856 #[test]
857 fn test_db_status() {
858 let mut db = ObjectDB::new();
859 assert_eq!(db.status(), "0 objects");
860 db.register("x", vec![]);
861 assert_eq!(db.status(), "1 objects");
862 }
863
864 #[test]
865 fn many_adoptions_share_one_holding_document() {
866 let parser = libxml::parser::Parser::default();
873 let mut db = ObjectDB::new();
874 let mut sources = Vec::new();
875 for i in 0..64 {
876 let doc = parser
877 .parse_string(format!("<title>Section <em>{i}</em></title>").as_bytes())
878 .expect("parse source");
879 let root = doc.get_root_element().expect("root");
880 let adopted = db.adopt_xml(&root).expect("adopt");
881 db.register(&format!("ID:S{i}"), vec![("title", adopted)]);
882 sources.push(doc);
883 }
884 drop(sources); for i in 0..64 {
886 let entry = db.lookup(&format!("ID:S{i}")).expect("entry");
887 let node = entry.get_xml("title").expect("stored node");
888 assert_eq!(node.get_content(), format!("Section {i}"));
889 }
890 let holder = db
893 .xml_holder
894 .as_ref()
895 .expect("holder exists after adoption");
896 let root = holder.get_root_element().expect("holder root");
897 assert_eq!(root.get_child_elements().len(), 64);
898 }
899
900 #[test]
901 fn adopted_xml_survives_source_document_drop() {
902 let parser = libxml::parser::Parser::default();
906 let source = parser
907 .parse_string(
908 "<title xmlns:m=\"http://www.w3.org/1998/Math/MathML\">\
909 The <m:mi>\u{03b1}</m:mi> section</title>",
910 )
911 .expect("parse source");
912 let title = source.get_root_element().expect("root");
913
914 let mut db = ObjectDB::new();
915 let adopted = db.adopt_xml(&title).expect("adopt");
916 db.register("ID:S1", vec![("title", adopted)]);
917 drop(source); let entry = db.lookup("ID:S1").expect("entry");
920 let node = entry.get_xml("title").expect("stored node");
921 assert_eq!(node.get_name(), "title");
922 assert_eq!(node.get_content(), "The \u{03b1} section");
923 let mi = node
926 .get_child_nodes()
927 .into_iter()
928 .find(|c| c.get_name() == "mi")
929 .expect("m:mi child survives");
930 assert_eq!(
931 mi.get_namespace().map(|ns| ns.get_href()),
932 Some(String::from("http://www.w3.org/1998/Math/MathML"))
933 );
934 }
935
936 #[test]
937 fn test_value_truthy() {
938 assert!(!Value::Null.is_truthy());
939 assert!(!Value::String(String::new()).is_truthy());
940 assert!(Value::String("hello".to_string()).is_truthy());
941 assert!(Value::Bool(true).is_truthy());
942 assert!(!Value::Bool(false).is_truthy());
943 assert!(Value::Int(42).is_truthy());
944 assert!(!Value::List(vec![]).is_truthy());
945 assert!(Value::List(vec![Value::Int(1)]).is_truthy());
946 }
947
948 #[test]
952 fn dbfile_round_trips_all_value_shapes() {
953 let dir = tempfile::tempdir().expect("tempdir");
954 let dbfile = dir.path().join("site.db");
955
956 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("attach fresh");
957 let title_doc = libxml::parser::Parser::default()
958 .parse_string("<ltx:text xmlns:ltx=\"http://dlmf.nist.gov/LaTeXML\">A <ltx:emph>title</ltx:emph></ltx:text>")
959 .expect("title parses");
960 let title = db
961 .adopt_xml(&title_doc.get_root_element().expect("root"))
962 .expect("adopted");
963 db.register("doc1#s1", vec![
964 ("type", Value::String("ltx:section".into())),
965 ("pageid", Value::Int(3)),
966 ("fresh", Value::Bool(true)),
967 (
968 "children",
969 Value::List(vec![
970 Value::String("doc1#s1.p1".into()),
971 Value::String("doc1#s1.p2".into()),
972 ]),
973 ),
974 (
975 "referrers",
976 Value::Hash({
977 let mut h = HashMap::default();
978 h.insert("doc2".to_string(), Value::String("ref".into()));
979 h
980 }),
981 ),
982 ("title", title),
983 ("missing", Value::Null),
984 ]);
985 let stored = db.finish().expect("finish writes");
986 assert_eq!(stored, 1, "one changed entry stored");
987
988 let mut reloaded = ObjectDB::attach(&dbfile, DbAttachOptions {
989 readonly: true,
990 ..Default::default()
991 })
992 .expect("attach readonly");
993 let entry = reloaded.lookup("doc1#s1").expect("entry survives");
994 assert_eq!(entry.get_string("type"), Some("ltx:section"));
995 assert!(matches!(entry.get_value("pageid"), Some(Value::Int(3))));
996 assert!(matches!(entry.get_value("fresh"), Some(Value::Bool(true))));
997 assert_eq!(entry.get_children(), vec!["doc1#s1.p1", "doc1#s1.p2"]);
998 assert!(matches!(entry.get_value("referrers"), Some(Value::Hash(_))));
999 let title = entry.get_xml("title").expect("XML value survives");
1000 assert!(
1001 title.get_content().contains("A title"),
1002 "adopted XML content round-trips"
1003 );
1004 assert_eq!(reloaded.finish().expect("readonly finish"), 0);
1005
1006 let mut again = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("attach rw");
1011 assert_eq!(
1012 again.finish().expect("idempotent finish"),
1013 0,
1014 "an untouched XML-bearing entry must not re-store"
1015 );
1016
1017 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("attach 3");
1020 db.unregister("doc1#s1");
1021 db.finish().expect("finish after unregister");
1022 let db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("attach 4");
1023 assert!(
1024 db.lookup("doc1#s1").is_none(),
1025 "an unregistered key must not resurrect on re-attach"
1026 );
1027 }
1028
1029 #[test]
1033 fn dbfile_finish_writes_only_changes() {
1034 let dir = tempfile::tempdir().expect("tempdir");
1035 let dbfile = dir.path().join("site.db");
1036 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("attach");
1037 db.register("a", vec![("v", Value::Int(1))]);
1038 db.register("b", vec![("v", Value::Int(2))]);
1039 assert_eq!(db.finish().expect("first finish"), 2);
1040
1041 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("re-attach");
1042 assert_eq!(
1043 db.finish().expect("no-op finish"),
1044 0,
1045 "unchanged DB stores nothing"
1046 );
1047
1048 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("re-attach 2");
1049 db.register("b", vec![("v", Value::Int(99))]);
1050 db.register("c", vec![("v", Value::Int(3))]);
1051 assert_eq!(
1052 db.finish().expect("delta finish"),
1053 2,
1054 "one changed + one new"
1055 );
1056 let db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("re-attach 3");
1057 assert!(matches!(
1058 db.lookup("b").and_then(|e| e.get_value("v")),
1059 Some(Value::Int(99))
1060 ));
1061 assert_eq!(db.len(), 3);
1062 }
1063
1064 #[test]
1071 fn dbfile_concurrent_readers_and_writer_contention() {
1072 let dir = tempfile::tempdir().expect("tempdir");
1073 let dbfile = dir.path().join("site.db");
1074 {
1075 let mut db = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("seed");
1076 for i in 0..500 {
1077 db.register(&format!("k{i}"), vec![("v", Value::Int(i))]);
1078 }
1079 assert_eq!(db.finish().expect("seed finish"), 500);
1080 }
1081
1082 let blocker = rusqlite::Connection::open(&dbfile).expect("blocker open");
1085 blocker
1086 .execute_batch("BEGIN IMMEDIATE; UPDATE entries SET props = props WHERE key = 'k0';")
1087 .expect("blocker tx");
1088 let release = std::thread::spawn(move || {
1089 std::thread::sleep(std::time::Duration::from_millis(300));
1090 blocker.execute_batch("COMMIT;").expect("blocker commit");
1091 });
1092 let mut writer = ObjectDB::attach(&dbfile, DbAttachOptions::default()).expect("writer");
1093 writer.register("k0", vec![("v", Value::Int(-1))]);
1094 let t0 = std::time::Instant::now();
1095 assert_eq!(
1096 writer.finish().expect("contended finish succeeds"),
1097 1,
1098 "the second writer stores its one change after the blocker commits"
1099 );
1100 assert!(
1101 t0.elapsed() < std::time::Duration::from_secs(9),
1102 "finish waited out the blocker, not the full timeout"
1103 );
1104 release.join().expect("blocker thread");
1105
1106 std::thread::scope(|scope| {
1108 let dbfile = &dbfile;
1109 let mut handles = Vec::new();
1110 for _ in 0..4 {
1111 handles.push(scope.spawn(move || {
1112 for _ in 0..10 {
1113 let db = ObjectDB::attach(dbfile, DbAttachOptions {
1114 readonly: true,
1115 ..Default::default()
1116 })
1117 .expect("reader attaches during writes");
1118 assert_eq!(db.len(), 500, "readers always see a full snapshot");
1119 assert!(db.lookup("k42").is_some());
1120 }
1121 }));
1122 }
1123 scope.spawn(move || {
1124 for round in 0..10 {
1125 let mut db =
1126 ObjectDB::attach(dbfile, DbAttachOptions::default()).expect("writer attaches");
1127 db.register("k1", vec![("v", Value::Int(1000 + round))]);
1128 db.finish().expect("interleaved writer finish");
1129 }
1130 });
1131 for h in handles {
1132 h.join().expect("reader thread");
1133 }
1134 });
1135 }
1136
1137 #[test]
1140 fn dbfile_version_mismatch_is_refused() {
1141 let dir = tempfile::tempdir().expect("tempdir");
1142 let dbfile = dir.path().join("site.db");
1143 {
1144 let conn = rusqlite::Connection::open(&dbfile).expect("open");
1145 conn
1146 .execute_batch("PRAGMA user_version = 999;")
1147 .expect("stamp");
1148 }
1149 let err = ObjectDB::attach(&dbfile, DbAttachOptions::default())
1150 .err()
1151 .expect("mismatched version must refuse");
1152 assert!(
1153 err.contains("format version 999"),
1154 "names the found version: {err}"
1155 );
1156 let db = ObjectDB::attach(&dbfile, DbAttachOptions {
1158 clean: true,
1159 ..Default::default()
1160 })
1161 .expect("clean rebuild attaches");
1162 assert!(db.is_empty());
1163 }
1164}