ursa_engine/database/
catalog_file.rs1use std::fs::File;
19use std::io::Write;
20use std::path::Path;
21
22use datafusion::error::{DataFusionError, Result};
23use serde_json::Value;
24
25pub const FORMAT: &str = "ursa";
27pub const VERSION: u64 = 1;
29
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct CatalogFile {
33 pub statements: Vec<String>,
34}
35
36impl CatalogFile {
37 pub fn parse(text: &str, name: &str) -> Result<Self> {
39 if text.trim().is_empty() {
40 return Ok(Self::default());
41 }
42 let invalid = |why: String| {
43 DataFusionError::Execution(format!("{name} is not a valid Ursa database: {why}"))
44 };
45 let value: Value =
46 serde_json::from_str(text).map_err(|e| invalid(format!("it is not JSON ({e})")))?;
47 let Value::Object(fields) = value else {
48 return Err(invalid("it is not a JSON object".into()));
49 };
50 if fields.get("format").and_then(Value::as_str) != Some(FORMAT) {
51 return Err(invalid(format!("its \"format\" is not \"{FORMAT}\"")));
52 }
53 let version = fields
54 .get("version")
55 .and_then(Value::as_u64)
56 .filter(|v| *v >= 1)
57 .ok_or_else(|| invalid("its \"version\" is not a positive integer".into()))?;
58 if version > VERSION {
59 return Err(DataFusionError::Execution(format!(
60 "{name} was written by a newer version of Ursa (database format version \
61 {version}; this version of ursa reads version {VERSION}); upgrade ursa to \
62 open it"
63 )));
64 }
65 let statements = fields
66 .get("statements")
67 .and_then(Value::as_array)
68 .ok_or_else(|| invalid("it has no \"statements\" list".into()))?
69 .iter()
70 .map(|s| s.as_str().map(str::to_string))
71 .collect::<Option<Vec<_>>>()
72 .ok_or_else(|| invalid("an entry of \"statements\" is not a string".into()))?;
73 Ok(Self { statements })
74 }
75
76 pub fn read(path: &Path, name: &str) -> Result<Self> {
78 let text = std::fs::read_to_string(path)
79 .map_err(|e| DataFusionError::Execution(format!("cannot read {name}: {e}")))?;
80 Self::parse(&text, name)
81 }
82
83 pub fn to_text(&self) -> String {
86 let quote = |s: &str| Value::String(s.to_string()).to_string();
87 let mut text = format!(
88 "{{\n \"format\": {},\n \"version\": {VERSION},\n \"statements\": [",
89 quote(FORMAT)
90 );
91 for (i, statement) in self.statements.iter().enumerate() {
92 text.push_str(if i == 0 { "\n " } else { ",\n " });
93 text.push_str("e(statement));
94 }
95 if !self.statements.is_empty() {
96 text.push_str("\n ");
97 }
98 text.push_str("]\n}\n");
99 text
100 }
101
102 pub fn write(&self, path: &Path, scratch_dir: &Path, name: &str) -> Result<()> {
106 let failed =
107 |e: std::io::Error| DataFusionError::Execution(format!("cannot save {name}: {e}"));
108 let file_name = path
109 .file_name()
110 .map(|n| n.to_string_lossy().into_owned())
111 .unwrap_or_else(|| "catalog".into());
112 let temp = scratch_dir.join(format!("{file_name}.tmp"));
113 {
114 let mut file = File::create(&temp).map_err(failed)?;
115 file.write_all(self.to_text().as_bytes()).map_err(failed)?;
116 file.sync_all().map_err(failed)?;
117 }
118 std::fs::rename(&temp, path).map_err(failed)?;
119 sync_dir(path.parent().unwrap_or(Path::new(".")));
120 Ok(())
121 }
122}
123
124fn sync_dir(dir: &Path) {
127 #[cfg(unix)]
128 if let Ok(dir) = File::open(dir) {
129 let _ = dir.sync_all();
130 }
131 #[cfg(not(unix))]
132 let _ = dir;
133}