1use async_trait::async_trait;
2use rusqlite::{params, Connection, OptionalExtension};
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5use std::sync::Mutex;
6use vfs::engine::error::{VfsError, VfsResult};
7use vfs::engine::mem::metadata_store::MetadataDump;
8use vfs::engine::mem::InMemoryMetadataStore;
9use vfs::engine::metadata::MetadataStore;
10use vfs::engine::types::{
11 BlockKey, ChunkEdit, ChunkRange, ChunkRef, CreateInodeAttrs, DentryStat, InodeMeta, InodePatch,
12 InodeType, SnapshotId, Storage, Timespec, DEFAULT_CHUNK_SIZE,
13};
14
15const LOCAL_FS_SCHEMA_VERSION_TABLE: &str = "agentos_fs_schema_version";
16
17struct LocalFsMigration {
18 version: i64,
19 statements: &'static str,
20}
21
22const LOCAL_FS_MIGRATIONS: &[LocalFsMigration] = &[
26 LocalFsMigration {
27 version: 1,
28 statements: r#"
29 CREATE TABLE agentos_fs_inodes (
30 ino INTEGER PRIMARY KEY CHECK (ino > 0),
31 kind INTEGER NOT NULL CHECK (kind IN (0, 1, 2)),
32 mode INTEGER NOT NULL CHECK (mode BETWEEN 0 AND 4294967295),
33 uid INTEGER NOT NULL CHECK (uid BETWEEN 0 AND 4294967295),
34 gid INTEGER NOT NULL CHECK (gid BETWEEN 0 AND 4294967295),
35 size INTEGER NOT NULL CHECK (size >= 0),
36 nlink INTEGER NOT NULL CHECK (nlink >= 0),
37 atime_ns INTEGER NOT NULL,
38 mtime_ns INTEGER NOT NULL,
39 ctime_ns INTEGER NOT NULL,
40 birthtime_ns INTEGER NOT NULL,
41 storage_mode INTEGER NOT NULL CHECK (storage_mode IN (0, 1, 2)),
42 storage_chunk_size INTEGER CHECK (
43 storage_chunk_size IS NULL OR
44 storage_chunk_size BETWEEN 1 AND 4294967295
45 ),
46 inline_content BLOB,
47 symlink_target TEXT,
48 CHECK (
49 (storage_mode = 0 AND storage_chunk_size IS NULL AND inline_content IS NULL) OR
50 (storage_mode = 1 AND storage_chunk_size IS NULL AND inline_content IS NOT NULL) OR
51 (storage_mode = 2 AND storage_chunk_size IS NOT NULL AND inline_content IS NULL)
52 ),
53 CHECK (
54 (kind = 2 AND symlink_target IS NOT NULL) OR
55 (kind <> 2 AND symlink_target IS NULL)
56 )
57 ) STRICT;
58 CREATE TABLE agentos_fs_dentries (
59 parent_ino INTEGER NOT NULL CHECK (parent_ino > 0),
60 name TEXT NOT NULL CHECK (length(name) > 0),
61 child_ino INTEGER NOT NULL CHECK (child_ino > 0),
62 kind INTEGER NOT NULL CHECK (kind IN (0, 1, 2)),
63 PRIMARY KEY (parent_ino, name)
64 ) STRICT;
65 CREATE INDEX agentos_fs_dentries_parent
66 ON agentos_fs_dentries(parent_ino);
67 CREATE TABLE agentos_fs_chunks (
68 ino INTEGER NOT NULL CHECK (ino > 0),
69 chunk_index INTEGER NOT NULL CHECK (chunk_index >= 0),
70 block_key TEXT NOT NULL CHECK (length(block_key) > 0),
71 len INTEGER NOT NULL CHECK (len BETWEEN 0 AND 4294967295),
72 PRIMARY KEY (ino, chunk_index)
73 ) STRICT;
74 CREATE TABLE agentos_fs_block_refs (
75 block_key TEXT PRIMARY KEY CHECK (length(block_key) > 0),
76 refcount INTEGER NOT NULL CHECK (refcount > 0)
77 ) STRICT;
78 CREATE TABLE agentos_fs_snapshots (
79 snapshot_id INTEGER PRIMARY KEY CHECK (snapshot_id > 0),
80 root_ino INTEGER NOT NULL CHECK (root_ino > 0),
81 created_ns INTEGER NOT NULL
82 ) STRICT;
83 "#,
84 },
85 LocalFsMigration {
86 version: 2,
87 statements: r#"
88 ALTER TABLE agentos_fs_inodes RENAME TO agentos_fs_inodes_v1;
89 CREATE TABLE agentos_fs_inodes (
90 ino INTEGER PRIMARY KEY CHECK (ino > 0),
91 kind INTEGER NOT NULL CHECK (kind IN (0, 1, 2, 3, 4, 5)),
92 mode INTEGER NOT NULL CHECK (mode BETWEEN 0 AND 4294967295),
93 uid INTEGER NOT NULL CHECK (uid BETWEEN 0 AND 4294967295),
94 gid INTEGER NOT NULL CHECK (gid BETWEEN 0 AND 4294967295),
95 size INTEGER NOT NULL CHECK (size >= 0),
96 nlink INTEGER NOT NULL CHECK (nlink >= 0),
97 atime_ns INTEGER NOT NULL,
98 mtime_ns INTEGER NOT NULL,
99 ctime_ns INTEGER NOT NULL,
100 birthtime_ns INTEGER NOT NULL,
101 storage_mode INTEGER NOT NULL CHECK (storage_mode IN (0, 1, 2)),
102 storage_chunk_size INTEGER CHECK (
103 storage_chunk_size IS NULL OR
104 storage_chunk_size BETWEEN 1 AND 4294967295
105 ),
106 inline_content BLOB,
107 symlink_target TEXT,
108 xattrs_json BLOB NOT NULL DEFAULT X'7B7D',
109 allocated_extents_json BLOB NOT NULL DEFAULT X'5B5D',
110 CHECK (
111 (storage_mode = 0 AND storage_chunk_size IS NULL AND inline_content IS NULL) OR
112 (storage_mode = 1 AND storage_chunk_size IS NULL AND inline_content IS NOT NULL) OR
113 (storage_mode = 2 AND storage_chunk_size IS NOT NULL AND inline_content IS NULL)
114 ),
115 CHECK (
116 (kind = 2 AND symlink_target IS NOT NULL) OR
117 (kind <> 2 AND symlink_target IS NULL)
118 )
119 ) STRICT;
120 INSERT INTO agentos_fs_inodes
121 (ino, kind, mode, uid, gid, size, nlink, atime_ns, mtime_ns, ctime_ns,
122 birthtime_ns, storage_mode, storage_chunk_size, inline_content, symlink_target,
123 xattrs_json, allocated_extents_json)
124 SELECT ino, kind, mode, uid, gid, size, nlink, atime_ns, mtime_ns, ctime_ns,
125 birthtime_ns, storage_mode, storage_chunk_size, inline_content, symlink_target,
126 X'7B7D',
127 CASE WHEN kind = 0 AND size > 0
128 THEN CAST(printf('[[0,%d]]', (size + 511) / 512) AS BLOB)
129 ELSE X'5B5D'
130 END
131 FROM agentos_fs_inodes_v1;
132 DROP TABLE agentos_fs_inodes_v1;
133 "#,
134 },
135];
136
137pub struct SqliteMetadataStore {
138 connection: Mutex<Connection>,
139 pending_write_count: Mutex<usize>,
140 inner: InMemoryMetadataStore,
141}
142
143const MAX_PENDING_WRITE_COMMITS: usize = 256;
144
145impl SqliteMetadataStore {
146 pub fn open(path: impl AsRef<Path>) -> VfsResult<Self> {
147 let connection = Connection::open(path)
148 .map_err(|err| VfsError::eio(format!("open SQLite metadata store: {err}")))?;
149 Self::from_connection(connection)
150 }
151
152 pub fn in_memory() -> VfsResult<Self> {
153 let connection = Connection::open_in_memory()
154 .map_err(|err| VfsError::eio(format!("open in-memory SQLite metadata store: {err}")))?;
155 Self::from_connection(connection)
156 }
157
158 fn from_connection(mut connection: Connection) -> VfsResult<Self> {
159 connection
160 .pragma_update(None, "journal_mode", "WAL")
161 .map_err(|err| VfsError::eio(format!("enable SQLite WAL mode: {err}")))?;
162 connection
163 .pragma_update(None, "synchronous", "NORMAL")
164 .map_err(|err| VfsError::eio(format!("configure SQLite synchronous mode: {err}")))?;
165 install_schema(&mut connection)?;
166 let dump = load_dump(&connection)?;
167 let is_new = dump.is_none();
168 let inner = dump
169 .map(InMemoryMetadataStore::from_dump)
170 .unwrap_or_default();
171 if is_new {
172 persist_dump(&mut connection, &inner.dump())?;
173 }
174 Ok(Self {
175 connection: Mutex::new(connection),
176 pending_write_count: Mutex::new(0),
177 inner,
178 })
179 }
180
181 pub fn has_schema(&self) -> VfsResult<bool> {
182 let connection = self.connection.lock().expect("sqlite mutex poisoned");
183 let count: i64 = connection
184 .query_row(
185 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('agentos_fs_inodes', 'agentos_fs_dentries', 'agentos_fs_chunks', 'agentos_fs_block_refs', 'agentos_fs_snapshots')",
186 [],
187 |row| row.get(0),
188 )
189 .map_err(|err| VfsError::eio(format!("inspect SQLite schema: {err}")))?;
190 Ok(count == 5)
191 }
192
193 fn persist(&self) -> VfsResult<()> {
194 self.flush_pending_writes()?;
195 let dump = self.inner.dump();
196 let mut connection = self.connection.lock().expect("sqlite mutex poisoned");
197 persist_dump(&mut connection, &dump)
198 }
199
200 fn persist_create(&self, parent: u64, name: &str, meta: &InodeMeta) -> VfsResult<()> {
201 self.flush_pending_writes()?;
202 let parent_meta = self.inner.inode_meta(parent)?;
203 let mut connection = self.connection.lock().expect("sqlite mutex poisoned");
204 let tx = connection
205 .transaction()
206 .map_err(|err| VfsError::eio(format!("begin SQLite create transaction: {err}")))?;
207 upsert_inode(&tx, &parent_meta)?;
208 upsert_inode(&tx, meta)?;
209 tx.execute(
210 "INSERT INTO agentos_fs_dentries (parent_ino, name, child_ino, kind) VALUES (?, ?, ?, ?)",
211 params![parent, name, meta.ino, kind_id(meta.kind)],
212 )
213 .map_err(|err| VfsError::eio(format!("persist SQLite dentry {name}: {err}")))?;
214 tx.commit()
215 .map_err(|err| VfsError::eio(format!("commit SQLite create transaction: {err}")))
216 }
217
218 fn persist_set_attr(&self, ino: u64, storage_changed: bool) -> VfsResult<()> {
219 let meta = self.inner.inode_meta(ino)?;
220 let mut pending = self
221 .pending_write_count
222 .lock()
223 .expect("sqlite pending-write mutex poisoned");
224 let mut connection = self.connection.lock().expect("sqlite mutex poisoned");
225 if *pending > 0 {
226 if let Err(error) = self.persist_set_attr_rows(&connection, &meta, storage_changed) {
227 let rollback_result = connection.execute_batch("ROLLBACK");
228 *pending = 0;
229 if let Err(rollback_error) = rollback_result {
230 return Err(VfsError::eio(format!(
231 "{error}; rollback batched SQLite setattr failed: {rollback_error}"
232 )));
233 }
234 return Err(error);
235 }
236 return Ok(());
237 }
238
239 let tx = connection
240 .transaction()
241 .map_err(|err| VfsError::eio(format!("begin SQLite setattr transaction: {err}")))?;
242 self.persist_set_attr_rows(&tx, &meta, storage_changed)?;
243 tx.commit()
244 .map_err(|err| VfsError::eio(format!("commit SQLite setattr transaction: {err}")))
245 }
246
247 fn persist_set_attr_rows(
248 &self,
249 connection: &Connection,
250 meta: &InodeMeta,
251 storage_changed: bool,
252 ) -> VfsResult<()> {
253 let mut affected_keys = BTreeSet::new();
254 if storage_changed {
255 let mut statement = connection
256 .prepare_cached("SELECT block_key FROM agentos_fs_chunks WHERE ino = ?")
257 .map_err(|err| VfsError::eio(format!("prepare setattr chunk lookup: {err}")))?;
258 let rows = statement
259 .query_map(params![meta.ino], |row| row.get::<_, String>(0))
260 .map_err(|err| VfsError::eio(format!("query setattr chunks: {err}")))?;
261 for row in rows {
262 affected_keys.insert(BlockKey(
263 row.map_err(|err| VfsError::eio(format!("read setattr chunk key: {err}")))?,
264 ));
265 }
266 }
267 upsert_inode(connection, meta)?;
268 if storage_changed {
269 connection
270 .execute(
271 "DELETE FROM agentos_fs_chunks WHERE ino = ?",
272 params![meta.ino],
273 )
274 .map_err(|err| VfsError::eio(format!("delete setattr chunks: {err}")))?;
275 for key in affected_keys {
276 let refcount = self.inner.refcount(&key);
277 if refcount == 0 {
278 connection
279 .execute(
280 "DELETE FROM agentos_fs_block_refs WHERE block_key = ?",
281 params![key.0],
282 )
283 .map_err(|err| {
284 VfsError::eio(format!("delete setattr block ref {}: {err}", key.0))
285 })?;
286 } else {
287 connection
288 .execute(
289 "INSERT INTO agentos_fs_block_refs (block_key, refcount) VALUES (?, ?)
290 ON CONFLICT(block_key) DO UPDATE SET refcount=excluded.refcount",
291 params![key.0, refcount],
292 )
293 .map_err(|err| {
294 VfsError::eio(format!("persist setattr block ref {}: {err}", key.0))
295 })?;
296 }
297 }
298 }
299 Ok(())
300 }
301
302 fn flush_pending_writes(&self) -> VfsResult<()> {
303 let mut pending = self
304 .pending_write_count
305 .lock()
306 .expect("sqlite pending-write mutex poisoned");
307 if *pending == 0 {
308 return Ok(());
309 }
310 let connection = self.connection.lock().expect("sqlite mutex poisoned");
311 connection
312 .execute_batch("COMMIT")
313 .map_err(|err| VfsError::eio(format!("commit pending SQLite writes: {err}")))?;
314 *pending = 0;
315 Ok(())
316 }
317
318 fn flush_durable(&self) -> VfsResult<()> {
319 self.flush_pending_writes()?;
320 let connection = self.connection.lock().expect("sqlite mutex poisoned");
321 connection
322 .execute_batch("PRAGMA wal_checkpoint(FULL)")
323 .map_err(|err| VfsError::eio(format!("checkpoint SQLite metadata WAL: {err}")))
324 }
325
326 fn persist_commit_write(
327 &self,
328 ino: u64,
329 edits: &[ChunkEdit],
330 old_size: u64,
331 new_size: u64,
332 chunk_size: u64,
333 ) -> VfsResult<()> {
334 let meta = self.inner.inode_meta(ino)?;
335 let keep_chunks = if new_size == 0 {
336 0
337 } else {
338 new_size.div_ceil(chunk_size)
339 };
340 let old_chunks = if old_size == 0 {
341 0
342 } else {
343 old_size.div_ceil(chunk_size)
344 };
345 let mut pending = self
346 .pending_write_count
347 .lock()
348 .expect("sqlite pending-write mutex poisoned");
349 let connection = self.connection.lock().expect("sqlite mutex poisoned");
350 if *pending == 0 {
351 connection.execute_batch("BEGIN IMMEDIATE").map_err(|err| {
352 VfsError::eio(format!("begin batched SQLite write transaction: {err}"))
353 })?;
354 }
355
356 let write_result = (|| -> VfsResult<()> {
357 let mut affected_keys = BTreeSet::new();
358 upsert_inode(&connection, &meta)?;
359 if new_size < old_size {
360 let mut statement = connection
361 .prepare_cached(
362 "SELECT block_key FROM agentos_fs_chunks WHERE ino = ? AND chunk_index >= ?",
363 )
364 .map_err(|err| {
365 VfsError::eio(format!("prepare truncated chunk lookup: {err}"))
366 })?;
367 let rows = statement
368 .query_map(params![ino, keep_chunks], |row| row.get::<_, String>(0))
369 .map_err(|err| VfsError::eio(format!("query truncated chunks: {err}")))?;
370 for row in rows {
371 affected_keys.insert(BlockKey(row.map_err(|err| {
372 VfsError::eio(format!("read truncated chunk key: {err}"))
373 })?));
374 }
375 }
376
377 for edit in edits.iter().filter(|edit| edit.index < keep_chunks) {
378 let previous = if edit.index >= old_chunks {
379 None
380 } else {
381 connection
382 .prepare_cached(
383 "SELECT block_key FROM agentos_fs_chunks WHERE ino = ? AND chunk_index = ?",
384 )
385 .map_err(|err| {
386 VfsError::eio(format!(
387 "prepare previous SQLite chunk lookup {ino}/{}: {err}",
388 edit.index
389 ))
390 })?
391 .query_row(params![ino, edit.index], |row| row.get::<_, String>(0))
392 .optional()
393 .map_err(|err| {
394 VfsError::eio(format!(
395 "query previous SQLite chunk {ino}/{}: {err}",
396 edit.index
397 ))
398 })?
399 };
400 if let Some(key) = previous {
401 affected_keys.insert(BlockKey(key));
402 }
403 affected_keys.insert(edit.key.clone());
404 }
405
406 if new_size < old_size {
407 connection
408 .prepare_cached(
409 "DELETE FROM agentos_fs_chunks WHERE ino = ? AND chunk_index >= ?",
410 )
411 .map_err(|err| VfsError::eio(format!("prepare truncated chunk delete: {err}")))?
412 .execute(params![ino, keep_chunks])
413 .map_err(|err| {
414 VfsError::eio(format!("delete truncated SQLite chunks: {err}"))
415 })?;
416 }
417 let mut insert_chunk = connection
418 .prepare_cached(
419 "INSERT INTO agentos_fs_chunks (ino, chunk_index, block_key, len) VALUES (?, ?, ?, ?)
420 ON CONFLICT(ino, chunk_index) DO UPDATE SET
421 block_key=excluded.block_key, len=excluded.len",
422 )
423 .map_err(|err| VfsError::eio(format!("prepare SQLite chunk upsert: {err}")))?;
424 for edit in edits.iter().filter(|edit| edit.index < keep_chunks) {
425 insert_chunk
426 .execute(params![ino, edit.index, edit.key.0, edit.len])
427 .map_err(|err| {
428 VfsError::eio(format!("persist SQLite chunk {ino}/{}: {err}", edit.index))
429 })?;
430 }
431
432 for key in affected_keys {
433 let refcount = self.inner.refcount(&key);
434 if refcount == 0 {
435 connection
436 .execute(
437 "DELETE FROM agentos_fs_block_refs WHERE block_key = ?",
438 params![key.0],
439 )
440 .map_err(|err| {
441 VfsError::eio(format!("delete SQLite block ref {}: {err}", key.0))
442 })?;
443 } else {
444 connection
445 .execute(
446 "INSERT INTO agentos_fs_block_refs (block_key, refcount) VALUES (?, ?)
447 ON CONFLICT(block_key) DO UPDATE SET refcount=excluded.refcount",
448 params![key.0, refcount],
449 )
450 .map_err(|err| {
451 VfsError::eio(format!("persist SQLite block ref {}: {err}", key.0))
452 })?;
453 }
454 }
455 Ok(())
456 })();
457
458 if let Err(error) = write_result {
459 let rollback_result = connection.execute_batch("ROLLBACK");
460 *pending = 0;
461 if let Err(rollback_error) = rollback_result {
462 return Err(VfsError::eio(format!(
463 "{error}; rollback batched SQLite writes failed: {rollback_error}"
464 )));
465 }
466 return Err(error);
467 }
468
469 *pending += 1;
470 if *pending >= MAX_PENDING_WRITE_COMMITS {
471 connection.execute_batch("COMMIT").map_err(|err| {
472 VfsError::eio(format!("commit bounded SQLite write batch: {err}"))
473 })?;
474 *pending = 0;
475 }
476 Ok(())
477 }
478}
479
480impl Drop for SqliteMetadataStore {
481 fn drop(&mut self) {
482 if let Err(error) = self.flush_pending_writes() {
483 eprintln!("failed to flush pending SQLite metadata writes during drop: {error}");
484 }
485 }
486}
487
488fn upsert_inode(connection: &Connection, meta: &InodeMeta) -> VfsResult<()> {
489 let (storage_mode, storage_chunk_size, inline_content) = match &meta.storage {
490 Storage::None => (0, None, None),
491 Storage::Inline(data) => (1, None, Some(data.as_slice())),
492 Storage::Chunked { chunk_size } => (2, Some(*chunk_size), None),
493 };
494 let xattrs_json = serde_json::to_vec(&meta.xattrs)
495 .map_err(|err| VfsError::eio(format!("serialize inode {} xattrs: {err}", meta.ino)))?;
496 let allocated_extents_json = serde_json::to_vec(&meta.allocated_extents).map_err(|err| {
497 VfsError::eio(format!(
498 "serialize inode {} allocation extents: {err}",
499 meta.ino
500 ))
501 })?;
502 connection
503 .execute(
504 "INSERT INTO agentos_fs_inodes
505 (ino, kind, mode, uid, gid, size, nlink, atime_ns, mtime_ns, ctime_ns, birthtime_ns,
506 storage_mode, storage_chunk_size, inline_content, symlink_target, xattrs_json,
507 allocated_extents_json)
508 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
509 ON CONFLICT(ino) DO UPDATE SET
510 kind=excluded.kind, mode=excluded.mode, uid=excluded.uid, gid=excluded.gid,
511 size=excluded.size, nlink=excluded.nlink, atime_ns=excluded.atime_ns,
512 mtime_ns=excluded.mtime_ns, ctime_ns=excluded.ctime_ns,
513 birthtime_ns=excluded.birthtime_ns, storage_mode=excluded.storage_mode,
514 storage_chunk_size=excluded.storage_chunk_size,
515 inline_content=excluded.inline_content, symlink_target=excluded.symlink_target,
516 xattrs_json=excluded.xattrs_json,
517 allocated_extents_json=excluded.allocated_extents_json",
518 params![
519 meta.ino,
520 kind_id(meta.kind),
521 meta.mode,
522 meta.uid,
523 meta.gid,
524 meta.size,
525 meta.nlink,
526 timespec_to_ns(meta.atime),
527 timespec_to_ns(meta.mtime),
528 timespec_to_ns(meta.ctime),
529 timespec_to_ns(meta.birthtime),
530 storage_mode,
531 storage_chunk_size,
532 inline_content,
533 meta.symlink_target,
534 xattrs_json,
535 allocated_extents_json,
536 ],
537 )
538 .map_err(|err| VfsError::eio(format!("persist SQLite inode {}: {err}", meta.ino)))?;
539 Ok(())
540}
541
542fn install_schema(connection: &mut Connection) -> VfsResult<()> {
543 install_schema_migrations(connection, LOCAL_FS_MIGRATIONS)
544}
545
546fn install_schema_migrations(
547 connection: &mut Connection,
548 migrations: &[LocalFsMigration],
549) -> VfsResult<()> {
550 validate_migration_ladder(migrations)?;
551 let latest_version = migrations.last().map_or(0, |migration| migration.version);
552 let tx = connection
553 .transaction()
554 .map_err(|err| VfsError::eio(format!("begin SQLite schema migration: {err}")))?;
555 tx.execute_batch(
556 "CREATE TABLE IF NOT EXISTS agentos_fs_schema_version (
557 singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
558 schema_version INTEGER NOT NULL CHECK (schema_version >= 0)
559 ) STRICT;",
560 )
561 .map_err(|err| VfsError::eio(format!("install SQLite schema version table: {err}")))?;
562
563 let row_count: i64 = tx
564 .query_row(
565 "SELECT COUNT(*) FROM agentos_fs_schema_version",
566 [],
567 |row| row.get(0),
568 )
569 .map_err(|err| VfsError::eio(format!("inspect SQLite schema version rows: {err}")))?;
570 let current_version = match row_count {
571 0 => 0,
572 1 => tx
573 .query_row(
574 "SELECT schema_version FROM agentos_fs_schema_version WHERE singleton = 1",
575 [],
576 |row| row.get::<_, i64>(0),
577 )
578 .map_err(|err| VfsError::eio(format!("read SQLite schema version: {err}")))?,
579 count => {
580 return Err(VfsError::eio(format!(
581 "{LOCAL_FS_SCHEMA_VERSION_TABLE} must contain at most one row; found {count}"
582 )))
583 }
584 };
585 if !(0..=latest_version).contains(¤t_version) {
586 return Err(VfsError::eio(format!(
587 "unsupported {LOCAL_FS_SCHEMA_VERSION_TABLE} version {current_version}; latest supported version is {latest_version}"
588 )));
589 }
590
591 for migration in migrations
592 .iter()
593 .filter(|migration| migration.version > current_version)
594 {
595 tx.execute_batch(migration.statements).map_err(|err| {
596 VfsError::eio(format!(
597 "apply SQLite filesystem migration {}: {err}",
598 migration.version
599 ))
600 })?;
601 tx.execute(
602 "INSERT INTO agentos_fs_schema_version (singleton, schema_version)
603 VALUES (1, ?1)
604 ON CONFLICT(singleton) DO UPDATE SET schema_version = excluded.schema_version",
605 [migration.version],
606 )
607 .map_err(|err| {
608 VfsError::eio(format!(
609 "record SQLite filesystem migration {}: {err}",
610 migration.version
611 ))
612 })?;
613 }
614
615 tx.commit()
616 .map_err(|err| VfsError::eio(format!("commit SQLite schema migration: {err}")))
617}
618
619fn validate_migration_ladder(migrations: &[LocalFsMigration]) -> VfsResult<()> {
620 for (index, migration) in migrations.iter().enumerate() {
621 let expected = i64::try_from(index + 1)
622 .map_err(|_| VfsError::eio("SQLite filesystem migration version overflow"))?;
623 if migration.version != expected {
624 return Err(VfsError::eio(format!(
625 "malformed SQLite filesystem migration ladder: expected version {expected}, found {}",
626 migration.version
627 )));
628 }
629 if migration.statements.trim().is_empty() {
630 return Err(VfsError::eio(format!(
631 "malformed SQLite filesystem migration ladder: version {expected} has no statements"
632 )));
633 }
634 }
635 Ok(())
636}
637
638fn load_dump(connection: &Connection) -> VfsResult<Option<MetadataDump>> {
639 let inode_count: i64 = connection
640 .query_row("SELECT COUNT(*) FROM agentos_fs_inodes", [], |row| {
641 row.get(0)
642 })
643 .map_err(|err| VfsError::eio(format!("count SQLite inodes: {err}")))?;
644 if inode_count == 0 {
645 return Ok(None);
646 }
647
648 let mut inodes = BTreeMap::new();
649 let mut next_ino = 1;
650 let mut statement = connection
651 .prepare(
652 "SELECT ino, kind, mode, uid, gid, size, nlink, atime_ns, mtime_ns, ctime_ns,
653 birthtime_ns, storage_mode, storage_chunk_size, inline_content, symlink_target,
654 xattrs_json, allocated_extents_json
655 FROM agentos_fs_inodes",
656 )
657 .map_err(|err| VfsError::eio(format!("prepare inode load: {err}")))?;
658 let rows = statement
659 .query_map([], |row| {
660 let ino: u64 = row.get(0)?;
661 let kind_id: i64 = row.get(1)?;
662 let storage_id: i64 = row.get(11)?;
663 let chunk_size: Option<u32> = row.get(12)?;
664 let inline_content: Option<Vec<u8>> = row.get(13)?;
665 let symlink_target: Option<String> = row.get(14)?;
666 let xattrs_json: Vec<u8> = row.get(15)?;
667 let allocated_extents_json: Vec<u8> = row.get(16)?;
668 let kind = match kind_id {
669 0 => InodeType::File,
670 1 => InodeType::Directory,
671 2 => InodeType::Symlink,
672 3 => InodeType::CharacterDevice,
673 4 => InodeType::BlockDevice,
674 _ => InodeType::Fifo,
675 };
676 let storage = match storage_id {
677 1 => Storage::Inline(inline_content.unwrap_or_default()),
678 2 => Storage::Chunked {
679 chunk_size: chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
680 },
681 _ => Storage::None,
682 };
683 Ok(InodeMeta {
684 ino,
685 kind,
686 mode: row.get(2)?,
687 uid: row.get(3)?,
688 gid: row.get(4)?,
689 size: row.get(5)?,
690 nlink: row.get(6)?,
691 atime: ns_to_timespec(row.get(7)?),
692 mtime: ns_to_timespec(row.get(8)?),
693 ctime: ns_to_timespec(row.get(9)?),
694 birthtime: ns_to_timespec(row.get(10)?),
695 storage,
696 symlink_target,
697 allocated_extents: serde_json::from_slice(&allocated_extents_json).map_err(
698 |error| {
699 rusqlite::Error::FromSqlConversionFailure(
700 allocated_extents_json.len(),
701 rusqlite::types::Type::Blob,
702 Box::new(error),
703 )
704 },
705 )?,
706 xattrs: serde_json::from_slice(&xattrs_json).map_err(|error| {
707 rusqlite::Error::FromSqlConversionFailure(
708 xattrs_json.len(),
709 rusqlite::types::Type::Blob,
710 Box::new(error),
711 )
712 })?,
713 })
714 })
715 .map_err(|err| VfsError::eio(format!("load SQLite inodes: {err}")))?;
716 for row in rows {
717 let meta = row.map_err(|err| VfsError::eio(format!("load SQLite inode row: {err}")))?;
718 next_ino = next_ino.max(meta.ino + 1);
719 inodes.insert(meta.ino, meta);
720 }
721
722 let mut dentries = BTreeMap::new();
723 let mut statement = connection
724 .prepare("SELECT parent_ino, name, child_ino FROM agentos_fs_dentries")
725 .map_err(|err| VfsError::eio(format!("prepare dentry load: {err}")))?;
726 let rows = statement
727 .query_map([], |row| {
728 Ok((
729 (row.get::<_, u64>(0)?, row.get::<_, String>(1)?),
730 row.get::<_, u64>(2)?,
731 ))
732 })
733 .map_err(|err| VfsError::eio(format!("load SQLite dentries: {err}")))?;
734 for row in rows {
735 let (key, value) =
736 row.map_err(|err| VfsError::eio(format!("load SQLite dentry row: {err}")))?;
737 dentries.insert(key, value);
738 }
739
740 let mut chunks = BTreeMap::new();
741 let mut statement = connection
742 .prepare("SELECT ino, chunk_index, block_key, len FROM agentos_fs_chunks")
743 .map_err(|err| VfsError::eio(format!("prepare chunk load: {err}")))?;
744 let rows = statement
745 .query_map([], |row| {
746 let index = row.get::<_, u64>(1)?;
747 Ok((
748 (row.get::<_, u64>(0)?, index),
749 ChunkRef {
750 index,
751 key: BlockKey(row.get(2)?),
752 len: row.get(3)?,
753 },
754 ))
755 })
756 .map_err(|err| VfsError::eio(format!("load SQLite chunks: {err}")))?;
757 for row in rows {
758 let (key, value) =
759 row.map_err(|err| VfsError::eio(format!("load SQLite chunk row: {err}")))?;
760 chunks.insert(key, value);
761 }
762
763 let mut block_refs = BTreeMap::new();
764 let mut statement = connection
765 .prepare("SELECT block_key, refcount FROM agentos_fs_block_refs")
766 .map_err(|err| VfsError::eio(format!("prepare block ref load: {err}")))?;
767 let rows = statement
768 .query_map([], |row| Ok((BlockKey(row.get(0)?), row.get::<_, u64>(1)?)))
769 .map_err(|err| VfsError::eio(format!("load SQLite block refs: {err}")))?;
770 for row in rows {
771 let (key, value) =
772 row.map_err(|err| VfsError::eio(format!("load SQLite block ref row: {err}")))?;
773 block_refs.insert(key, value);
774 }
775
776 Ok(Some(MetadataDump {
777 next_ino,
778 inodes,
779 dentries,
780 chunks,
781 block_refs,
782 }))
783}
784
785fn persist_dump(connection: &mut Connection, dump: &MetadataDump) -> VfsResult<()> {
786 let tx = connection
787 .transaction()
788 .map_err(|err| VfsError::eio(format!("begin SQLite metadata transaction: {err}")))?;
789 tx.execute_batch(
790 "
791 DELETE FROM agentos_fs_snapshots;
792 DELETE FROM agentos_fs_block_refs;
793 DELETE FROM agentos_fs_chunks;
794 DELETE FROM agentos_fs_dentries;
795 DELETE FROM agentos_fs_inodes;
796 ",
797 )
798 .map_err(|err| VfsError::eio(format!("clear SQLite metadata tables: {err}")))?;
799
800 for meta in dump.inodes.values() {
801 upsert_inode(&tx, meta)?;
802 }
803
804 for ((parent, name), child) in &dump.dentries {
805 let kind = dump
806 .inodes
807 .get(child)
808 .map(|meta| meta.kind)
809 .ok_or_else(|| VfsError::eio(format!("dentry points to missing inode {child}")))?;
810 tx.execute(
811 "INSERT INTO agentos_fs_dentries (parent_ino, name, child_ino, kind) VALUES (?, ?, ?, ?)",
812 params![parent, name, child, kind_id(kind)],
813 )
814 .map_err(|err| VfsError::eio(format!("persist SQLite dentry {name}: {err}")))?;
815 }
816
817 for ((ino, index), chunk) in &dump.chunks {
818 tx.execute(
819 "INSERT INTO agentos_fs_chunks (ino, chunk_index, block_key, len) VALUES (?, ?, ?, ?)",
820 params![ino, index, chunk.key.0, chunk.len],
821 )
822 .map_err(|err| VfsError::eio(format!("persist SQLite chunk {ino}/{index}: {err}")))?;
823 }
824
825 for (key, refcount) in &dump.block_refs {
826 tx.execute(
827 "INSERT INTO agentos_fs_block_refs (block_key, refcount) VALUES (?, ?)",
828 params![key.0, refcount],
829 )
830 .map_err(|err| VfsError::eio(format!("persist SQLite block ref {}: {err}", key.0)))?;
831 }
832
833 tx.commit()
834 .map_err(|err| VfsError::eio(format!("commit SQLite metadata transaction: {err}")))
835}
836
837fn kind_id(kind: InodeType) -> i64 {
838 match kind {
839 InodeType::File => 0,
840 InodeType::Directory => 1,
841 InodeType::Symlink => 2,
842 InodeType::CharacterDevice => 3,
843 InodeType::BlockDevice => 4,
844 InodeType::Fifo => 5,
845 }
846}
847
848fn timespec_to_ns(time: Timespec) -> i64 {
849 time.sec.saturating_mul(1_000_000_000) + i64::from(time.nsec)
850}
851
852fn ns_to_timespec(ns: i64) -> Timespec {
853 Timespec {
854 sec: ns / 1_000_000_000,
855 nsec: ns.rem_euclid(1_000_000_000) as u32,
856 }
857}
858
859#[async_trait]
860impl MetadataStore for SqliteMetadataStore {
861 async fn resolve(&self, path: &str) -> VfsResult<InodeMeta> {
862 self.inner.resolve(path).await
863 }
864
865 async fn resolve_parent(&self, path: &str) -> VfsResult<(InodeMeta, String)> {
866 self.inner.resolve_parent(path).await
867 }
868
869 async fn lstat(&self, path: &str) -> VfsResult<InodeMeta> {
870 self.inner.lstat(path).await
871 }
872
873 async fn list_dir(&self, ino: u64) -> VfsResult<Vec<DentryStat>> {
874 self.inner.list_dir(ino).await
875 }
876
877 async fn create(
878 &self,
879 parent: u64,
880 name: &str,
881 attrs: CreateInodeAttrs,
882 ) -> VfsResult<InodeMeta> {
883 let result = self.inner.create(parent, name, attrs).await;
884 if let Ok(meta) = &result {
885 self.persist_create(parent, name, meta)?;
886 }
887 result
888 }
889
890 async fn link(&self, parent: u64, name: &str, target: u64) -> VfsResult<()> {
891 let result = self.inner.link(parent, name, target).await;
892 if result.is_ok() {
893 self.persist()?;
894 }
895 result
896 }
897
898 async fn remove(&self, parent: u64, name: &str) -> VfsResult<Vec<BlockKey>> {
899 let result = self.inner.remove(parent, name).await;
900 if result.is_ok() {
901 self.persist()?;
902 }
903 result
904 }
905
906 async fn rename(
907 &self,
908 src_parent: u64,
909 src: &str,
910 dst_parent: u64,
911 dst: &str,
912 ) -> VfsResult<Vec<BlockKey>> {
913 let result = self.inner.rename(src_parent, src, dst_parent, dst).await;
914 if result.is_ok() {
915 self.persist()?;
916 }
917 result
918 }
919
920 async fn set_attr(&self, ino: u64, patch: InodePatch) -> VfsResult<Vec<BlockKey>> {
921 let storage_changed = patch.storage.is_some();
922 let result = self.inner.set_attr(ino, patch).await;
923 if result.is_ok() {
924 self.persist_set_attr(ino, storage_changed)?;
925 }
926 result
927 }
928
929 async fn commit_write(
930 &self,
931 ino: u64,
932 edits: Vec<ChunkEdit>,
933 new_size: u64,
934 allocated_extents: Vec<(u64, u64)>,
935 ) -> VfsResult<Vec<BlockKey>> {
936 let chunk_size = match self.inner.inode_meta(ino)?.storage {
937 Storage::Chunked { chunk_size } => u64::from(chunk_size),
938 Storage::Inline(_) | Storage::None => u64::from(DEFAULT_CHUNK_SIZE),
939 };
940 let old_size = self.inner.inode_meta(ino)?.size;
941 let persisted_edits = edits.clone();
942 let result = self
943 .inner
944 .commit_write(ino, edits, new_size, allocated_extents)
945 .await;
946 if result.is_ok() {
947 self.persist_commit_write(ino, &persisted_edits, old_size, new_size, chunk_size)?;
948 }
949 result
950 }
951
952 async fn get_chunks(&self, ino: u64, range: ChunkRange) -> VfsResult<Vec<ChunkRef>> {
953 self.inner.get_chunks(ino, range).await
954 }
955
956 async fn snapshot(&self, root: u64) -> VfsResult<SnapshotId> {
957 self.inner.snapshot(root).await
958 }
959
960 async fn fork(&self, snap: SnapshotId) -> VfsResult<u64> {
961 let result = self.inner.fork(snap).await;
962 if result.is_ok() {
963 self.persist()?;
964 }
965 result
966 }
967
968 async fn gc(&self) -> VfsResult<Vec<BlockKey>> {
969 self.inner.gc().await
970 }
971
972 async fn flush(&self) -> VfsResult<()> {
973 self.flush_durable()
974 }
975}
976
977#[cfg(test)]
978mod writeback_tests {
979 use super::*;
980
981 #[test]
982 fn file_store_uses_writeback_sqlite_settings() {
983 let temp = tempfile::tempdir().unwrap();
984 let store = SqliteMetadataStore::open(temp.path().join("metadata.sqlite")).unwrap();
985 let connection = store.connection.lock().expect("sqlite mutex poisoned");
986 let journal_mode: String = connection
987 .query_row("PRAGMA journal_mode", [], |row| row.get(0))
988 .unwrap();
989 let synchronous: i64 = connection
990 .query_row("PRAGMA synchronous", [], |row| row.get(0))
991 .unwrap();
992
993 assert_eq!(journal_mode, "wal");
994 assert_eq!(synchronous, 1);
995 }
996}
997
998#[cfg(test)]
999mod tests {
1000 use super::*;
1001
1002 #[test]
1003 fn rejects_malformed_ladder_before_touching_database() {
1004 const MALFORMED: &[LocalFsMigration] = &[LocalFsMigration {
1005 version: 2,
1006 statements: "CREATE TABLE agentos_fs_probe (value INTEGER) STRICT;",
1007 }];
1008 let mut connection = Connection::open_in_memory().expect("open database");
1009
1010 let error = install_schema_migrations(&mut connection, MALFORMED)
1011 .expect_err("malformed ladder must fail");
1012
1013 assert!(error.message().contains("expected version 1, found 2"));
1014 let table_count: i64 = connection
1015 .query_row(
1016 "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name LIKE 'agentos_fs_%'",
1017 [],
1018 |row| row.get(0),
1019 )
1020 .expect("inspect database");
1021 assert_eq!(table_count, 0);
1022 }
1023
1024 #[test]
1025 fn rolls_back_schema_and_version_when_migration_fails() {
1026 const FAILING: &[LocalFsMigration] = &[LocalFsMigration {
1027 version: 1,
1028 statements: "CREATE TABLE agentos_fs_probe (value INTEGER CHECK (value > 0)) STRICT;
1029 INSERT INTO agentos_fs_probe (value) VALUES (0);",
1030 }];
1031 let mut connection = Connection::open_in_memory().expect("open database");
1032
1033 install_schema_migrations(&mut connection, FAILING)
1034 .expect_err("failing migration must roll back");
1035
1036 let table_count: i64 = connection
1037 .query_row(
1038 "SELECT COUNT(*) FROM sqlite_schema WHERE type = 'table' AND name IN ('agentos_fs_schema_version', 'agentos_fs_probe')",
1039 [],
1040 |row| row.get(0),
1041 )
1042 .expect("inspect database");
1043 assert_eq!(table_count, 0);
1044 }
1045}