ursa_engine/database/
catalog_file.rs1use std::collections::BTreeMap;
28use std::fs::File;
29use std::io::Write;
30use std::path::Path;
31
32use datafusion::error::{DataFusionError, Result};
33use serde_json::Value;
34
35pub const FORMAT: &str = "ursa";
37pub const VERSION: u64 = 1;
39
40#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct CatalogFile {
43 pub statements: Vec<String>,
44 pub settings: BTreeMap<String, String>,
47}
48
49impl CatalogFile {
50 pub fn parse(text: &str, name: &str) -> Result<Self> {
52 if text.trim().is_empty() {
53 return Ok(Self::default());
54 }
55 let invalid = |why: String| {
56 DataFusionError::Execution(format!("{name} is not a valid Ursa database: {why}"))
57 };
58 let value: Value =
59 serde_json::from_str(text).map_err(|e| invalid(format!("it is not JSON ({e})")))?;
60 let Value::Object(fields) = value else {
61 return Err(invalid("it is not a JSON object".into()));
62 };
63 if fields.get("format").and_then(Value::as_str) != Some(FORMAT) {
64 return Err(invalid(format!("its \"format\" is not \"{FORMAT}\"")));
65 }
66 let version = fields
67 .get("version")
68 .and_then(Value::as_u64)
69 .filter(|v| *v >= 1)
70 .ok_or_else(|| invalid("its \"version\" is not a positive integer".into()))?;
71 if version > VERSION {
72 return Err(DataFusionError::Execution(format!(
73 "{name} was written by a newer version of Ursa (database format version \
74 {version}; this version of ursa reads version {VERSION}); upgrade ursa to \
75 open it"
76 )));
77 }
78 let statements = fields
79 .get("statements")
80 .and_then(Value::as_array)
81 .ok_or_else(|| invalid("it has no \"statements\" list".into()))?
82 .iter()
83 .map(|s| s.as_str().map(str::to_string))
84 .collect::<Option<Vec<_>>>()
85 .ok_or_else(|| invalid("an entry of \"statements\" is not a string".into()))?;
86 let settings = match fields.get("settings") {
87 None | Some(Value::Null) => BTreeMap::new(),
88 Some(Value::Object(settings)) => settings
89 .iter()
90 .map(|(k, v)| v.as_str().map(|v| (k.clone(), v.to_string())))
91 .collect::<Option<BTreeMap<_, _>>>()
92 .ok_or_else(|| invalid("a value of \"settings\" is not a string".into()))?,
93 Some(_) => return Err(invalid("its \"settings\" is not a JSON object".into())),
94 };
95 Ok(Self {
96 statements,
97 settings,
98 })
99 }
100
101 pub fn read(path: &Path, name: &str) -> Result<Self> {
103 let text = std::fs::read_to_string(path)
104 .map_err(|e| DataFusionError::Execution(format!("cannot read {name}: {e}")))?;
105 Self::parse(&text, name)
106 }
107
108 pub fn to_text(&self) -> String {
112 let quote = |s: &str| Value::String(s.to_string()).to_string();
113 let mut text = format!(
114 "{{\n \"format\": {},\n \"version\": {VERSION},\n \"statements\": [",
115 quote(FORMAT)
116 );
117 for (i, statement) in self.statements.iter().enumerate() {
118 text.push_str(if i == 0 { "\n " } else { ",\n " });
119 text.push_str("e(statement));
120 }
121 if !self.statements.is_empty() {
122 text.push_str("\n ");
123 }
124 text.push(']');
125 if !self.settings.is_empty() {
126 text.push_str(",\n \"settings\": {");
127 for (i, (name, value)) in self.settings.iter().enumerate() {
128 text.push_str(if i == 0 { "\n " } else { ",\n " });
129 text.push_str(&format!("{}: {}", quote(name), quote(value)));
130 }
131 text.push_str("\n }");
132 }
133 text.push_str("\n}\n");
134 text
135 }
136
137 pub fn write(&self, path: &Path, scratch_dir: &Path, name: &str) -> Result<()> {
141 let failed =
142 |e: std::io::Error| DataFusionError::Execution(format!("cannot save {name}: {e}"));
143 let file_name = path
144 .file_name()
145 .map(|n| n.to_string_lossy().into_owned())
146 .unwrap_or_else(|| "catalog".into());
147 let temp = scratch_dir.join(format!("{file_name}.tmp"));
148 {
149 let mut file = File::create(&temp).map_err(failed)?;
150 file.write_all(self.to_text().as_bytes()).map_err(failed)?;
151 file.sync_all().map_err(failed)?;
152 }
153 std::fs::rename(&temp, path).map_err(failed)?;
154 sync_dir(path.parent().unwrap_or(Path::new(".")));
155 Ok(())
156 }
157}
158
159fn sync_dir(dir: &Path) {
162 #[cfg(unix)]
163 if let Ok(dir) = File::open(dir) {
164 let _ = dir.sync_all();
165 }
166 #[cfg(not(unix))]
167 let _ = dir;
168}