recern_vector/
database.rs1use crate::collection::{Collection, CollectionConfig};
2use crate::error::{Error, Result};
3use crate::{storage, wal};
4use std::collections::BTreeMap;
5use std::fs::{self, File, OpenOptions};
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::sync::Mutex;
9
10pub struct Database {
14 path: PathBuf,
15 collections: BTreeMap<String, Collection>,
16 persistence: Mutex<Vec<u8>>,
17 read_only: bool,
18}
19struct Lock(File);
20impl Lock {
21 fn write(path: &Path) -> Result<Self> {
22 let file = OpenOptions::new()
23 .create(true)
24 .truncate(false)
25 .read(true)
26 .write(true)
27 .open(wal::sidecar(path, ".lock"))?;
28 file.lock()?;
29 Ok(Self(file))
30 }
31 fn read(path: &Path) -> Result<Option<Self>> {
32 match File::open(wal::sidecar(path, ".lock")) {
33 Ok(file) => {
34 file.lock_shared()?;
35 Ok(Some(Self(file)))
36 }
37 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
38 Err(e) => Err(e.into()),
39 }
40 }
41}
42impl Drop for Lock {
43 fn drop(&mut self) {
44 let _ = self.0.unlock();
45 }
46}
47
48impl Database {
49 pub fn create(path: impl AsRef<Path>) -> Result<Self> {
50 let path = absolute(path.as_ref())?;
51 let _lock = Lock::write(&path)?;
52 if path.exists() {
53 return Err(Error::FileExists(path));
54 }
55 if wal::sidecar(&path, ".wal").exists() {
56 return Err(Error::InvalidArgument(
57 "orphan WAL exists; recover or remove it before creating a database".into(),
58 ));
59 }
60 let collections = BTreeMap::new();
61 let bytes = storage::encode(&collections, identity(), 0);
62 atomic_snapshot(&path, &bytes)?;
63 Ok(Self {
64 path,
65 collections,
66 persistence: Mutex::new(bytes),
67 read_only: false,
68 })
69 }
70 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
71 Self::load(path.as_ref(), false)
72 }
73 pub fn open_read_only(path: impl AsRef<Path>) -> Result<Self> {
76 Self::load(path.as_ref(), true)
77 }
78 fn load(path: &Path, read_only: bool) -> Result<Self> {
79 let path = fs::canonicalize(path)?;
80 let _lock = if read_only {
81 Lock::read(&path)?
82 } else {
83 Some(Lock::write(&path)?)
84 };
85 let loaded = wal::load(&path)?;
86 let collections = storage::decode(&loaded.bytes)?;
87 Ok(Self {
88 path,
89 collections,
90 persistence: Mutex::new(loaded.bytes),
91 read_only,
92 })
93 }
94 pub fn open_or_create(path: impl AsRef<Path>) -> Result<Self> {
95 match Self::open(&path) {
96 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
97 match Self::create(&path) {
98 Err(Error::FileExists(_)) => Self::open(path),
99 result => result,
100 }
101 }
102 result => result,
103 }
104 }
105 pub fn format_version(&self) -> u32 {
107 let bytes = self
108 .persistence
109 .lock()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 u32::from_le_bytes(bytes[4..8].try_into().expect("validated header"))
112 }
113 pub fn is_read_only(&self) -> bool {
114 self.read_only
115 }
116
117 pub fn identity(&self) -> u128 {
120 let bytes = self
121 .persistence
122 .lock()
123 .unwrap_or_else(std::sync::PoisonError::into_inner);
124 storage::header(&bytes).0
125 }
126
127 pub fn sequence(&self) -> u64 {
129 let bytes = self
130 .persistence
131 .lock()
132 .unwrap_or_else(std::sync::PoisonError::into_inner);
133 storage::header(&bytes).1
134 }
135 fn writable(&self) -> Result<()> {
136 if self.read_only {
137 Err(Error::InvalidArgument("database is read-only".into()))
138 } else {
139 Ok(())
140 }
141 }
142 pub fn path(&self) -> &Path {
143 &self.path
144 }
145
146 pub fn create_collection(
147 &mut self,
148 name: &str,
149 config: CollectionConfig,
150 ) -> Result<&mut Collection> {
151 self.writable()?;
152 if name.is_empty() || name.len() > 255 {
153 return Err(Error::InvalidArgument(
154 "collection name must be 1-255 bytes".into(),
155 ));
156 }
157 if self.collections.contains_key(name) {
158 return Err(Error::CollectionExists(name.to_owned()));
159 }
160 let collection = Collection::new(name, config)?;
161 Ok(self
162 .collections
163 .entry(name.to_owned())
164 .or_insert(collection))
165 }
166
167 pub fn collection(&self, name: &str) -> Result<&Collection> {
168 self.collections
169 .get(name)
170 .ok_or_else(|| Error::CollectionNotFound(name.to_owned()))
171 }
172
173 pub fn collection_mut(&mut self, name: &str) -> Result<&mut Collection> {
174 self.writable()?;
175 self.collections
176 .get_mut(name)
177 .ok_or_else(|| Error::CollectionNotFound(name.to_owned()))
178 }
179
180 pub fn drop_collection(&mut self, name: &str) -> Result<()> {
181 self.writable()?;
182 self.collections
183 .remove(name)
184 .map(|_| ())
185 .ok_or_else(|| Error::CollectionNotFound(name.to_owned()))
186 }
187
188 pub fn collections(&self) -> impl Iterator<Item = &Collection> {
189 self.collections.values()
190 }
191
192 pub fn save(&self) -> Result<()> {
195 self.persist(false)
196 }
197 pub fn checkpoint(&self) -> Result<()> {
200 self.persist(true)
201 }
202 fn persist(&self, checkpoint: bool) -> Result<()> {
203 self.writable()?;
204 let mut previous = self
205 .persistence
206 .lock()
207 .map_err(|_| Error::InvalidArgument("persistence lock poisoned".into()))?;
208 let _lock = Lock::write(&self.path)?;
209 let disk = wal::load(&self.path)?;
210 if disk.bytes != *previous {
211 return Err(Error::InvalidArgument(
212 "database changed in another handle; reopen before writing".into(),
213 ));
214 }
215 let (mut id, seq) = storage::header(&previous);
216 let migrate = id == 0;
217 if migrate {
218 id = identity();
219 }
220 let unchanged = storage::encode(&self.collections, id, seq) == *previous;
221 if unchanged && !checkpoint {
222 return Ok(());
223 }
224 let next = if unchanged {
225 seq
226 } else {
227 seq.checked_add(1)
228 .ok_or_else(|| Error::InvalidArgument("commit sequence exhausted".into()))?
229 };
230 let bytes = storage::encode(&self.collections, id, next);
231 if checkpoint || migrate {
232 atomic_snapshot(&self.path, &bytes)?;
233 *previous = bytes;
236 atomic_snapshot(&wal::sidecar(&self.path, ".wal"), &wal::header(id))?;
237 sync_parent_dir(&self.path);
238 } else {
239 let frame = wal::frame(&previous, &bytes, next);
240 wal::append(&self.path, disk.wal_len, id, &frame)?;
241 *previous = bytes;
242 }
243 Ok(())
244 }
245}
246fn absolute(path: &Path) -> Result<PathBuf> {
247 if path.exists() {
248 return Ok(fs::canonicalize(path)?);
249 }
250 let parent = path
251 .parent()
252 .filter(|p| !p.as_os_str().is_empty())
253 .unwrap_or(Path::new("."));
254 Ok(fs::canonicalize(parent)?.join(
255 path.file_name()
256 .ok_or_else(|| Error::InvalidArgument("file name required".into()))?,
257 ))
258}
259fn identity() -> u128 {
260 use std::sync::atomic::{AtomicU64, Ordering};
261 static NEXT: AtomicU64 = AtomicU64::new(1);
262 let time = std::time::SystemTime::now()
263 .duration_since(std::time::UNIX_EPOCH)
264 .unwrap_or_default()
265 .as_nanos();
266 time ^ ((std::process::id() as u128) << 64) ^ NEXT.fetch_add(1, Ordering::Relaxed) as u128
267}
268fn atomic_snapshot(path: &Path, bytes: &[u8]) -> Result<()> {
269 let tmp = wal::sidecar(path, &format!(".{}.tmp", identity()));
270 let result = (|| {
271 let mut file = OpenOptions::new().create_new(true).write(true).open(&tmp)?;
272 file.write_all(bytes)?;
273 file.sync_all()?;
274 fs::rename(&tmp, path)?;
275 sync_parent_dir(path);
276 Ok(())
277 })();
278 if result.is_err() {
279 let _ = fs::remove_file(tmp);
280 }
281 result
282}
283pub(crate) fn sync_parent_dir(path: &Path) {
284 let dir = path
285 .parent()
286 .filter(|p| !p.as_os_str().is_empty())
287 .unwrap_or(Path::new("."));
288 if let Ok(dir) = File::open(dir) {
289 let _ = dir.sync_all();
290 }
291}