# inode-db

An inode-style embedded key/value database written in Rust. Records are
stored as fixed-size inodes inside 4 KB blocks; the index is a
Copy-on-Write tree updated via an atomic root-swap; durability is
guaranteed by a CRC-32 framed Write-Ahead Log.

This crate ships two halves:

* **`inode-db`** — the runtime library (`Database`, storage, WAL,
  recovery, OIM session).
* **`inode-db-derive`** — the `#[derive(InodeModel)]` proc macro that
  generates the trait implementation for your model structs.

## Quick start

Add to `Cargo.toml`:

```toml
[dependencies]
inode-db = "0.1"
inode-db-derive = "0.1"
```

Then in your code:

```rust
use inode_db::{Database, oim::InodeModel};
use inode_db_derive::InodeModel;
use serde::{Deserialize, Serialize};

#[derive(InodeModel, Clone, Debug, Serialize, Deserialize)]
#[oim(table = "users")]
struct User {
    #[oim(primary_key)]
    id: u64,
    #[oim(index)]
    email: String,
    name: String,
}

let db = Database::memory();
db.create_table::<User>()?;
db.insert(&User { id: 1, email: "a@x".into(), name: "Alice".into() })?;
let alice = db.get::<User>(&1)?.unwrap();
```

To persist to disk:

```rust
let db = Database::file("/tmp/my-database.inode-db")?;
// ... writes are fsynced via the WAL on every commit ...
drop(db);

let db = Database::file("/tmp/my-database.inode-db")?;
// alice is still here.
let alice = db.get::<User>(&1)?.unwrap();
```

## Architecture

* **Block layer** (`block.rs`) — 4 KB immutable blocks.
* **Inode layer** (`inode.rs`) — fixed-size `InodePayload` with 12
  direct + single/double/triple indirect block pointers; one inode per
  block in v0.1.
* **Storage layer** (`store.rs`) — `Memory` or `File` backend, free
  list, superblock with magic + version + `root_inode_id`.
* **WAL** (`wal.rs`) — length-prefixed, CRC-32 framed records; every
  append is `fdatasync`'d.
* **Recovery** (`recovery.rs`) — replays the WAL on open, applies
  committed transactions, aborts uncommitted ones, truncates the log.
* **OIM** (`oim/`) — table-name → root-inode-id directory plus
  per-table indexes; `OimContext` is the read/write API surface.

### Write path

1. `begin_txn` — `Begin` record appended to WAL, fsync.
2. Allocate fresh blocks; write data into them.
3. Build a new table-root inode; build a new primary-root inode.
4. `Commit { new_root, freed }` record appended, fsync.
5. Update superblock, fsync.
6. Atomic `Ordering::Release` swap of `root_inode_id`.
7. Free the old blocks.

Readers see the old root or the new root, never partial.

### Recovery

* Committed transactions past `last_committed_txn` are applied
  (advance root, free their `freed` blocks).
* Uncommitted transactions are aborted: their `Alloc` block ids go
  straight to the free list.
* The WAL is truncated after a clean replay.

## Limitations (v0.1)

* Values are stored in direct pointers only — the cap is
  `DIRECT_COUNT * BLOCK_SIZE = 48 KB` per record. Single/double/triple
  indirect support is reserved in the on-disk format but not exercised
  in v0.1.
* Whole-index CoW: every write rebuilds the per-table index.
  O(N) bytes per write. Fine for tens of thousands of rows; B-tree
  page-level CoW is a v0.2 goal.
* Secondary indexes are *recorded* by the OIM but not yet consulted on
  read.
* Single-process only. SWMR per-process via the atomic root id.
* Linux-only fsync semantics: `File::sync_data()` is `fdatasync` on
  Linux, weaker on macOS.

## License

Dual-licensed under MIT or Apache-2.0, at your option.