El crate és arkeion a crates.io — la versió actual és la v0.12.

Incrustat — dins del procés la teva app Arkeion (biblioteca) crida fn db.arkeion Client / servidor — sobre TCP la teva app arkeion-client arkeiond incrusta Arkeion TCP db.arkeion
El mateix motor i el mateix fitxer, assolits de dues maneres. Tot el que segueix descriu l'API incrustada; la capa de xarxa en reflecteix un subconjunt.

Tipus principals

#![forbid(unsafe_code)]

pub struct Database;            // shared, cloneable handle (internal Arc)
pub struct Connection;          // view over a branch (defaults to "main")
pub struct Transaction<'conn>;  // explicit multi-statement write
pub struct Rows;                // result iterator
pub struct Row;

#[derive(Clone, Debug, PartialEq)]
pub enum Value { Null, Bool(bool), Integer(i64), Real(f64), Text(String), Blob(Vec<u8>) }

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version(pub u64);

#[derive(Clone, Copy)]
pub enum AsOf { Head, Version(u64), Timestamp(std::time::SystemTime) }

pub struct Options {
    pub create_if_missing: bool,   // true by default (zero-config)
    pub key: Option<Key>,          // Some(_) => AES-256-GCM at rest
    pub compress: bool,            // page compression on create (off by default)
    pub ecc_nsym: u8,              // Reed-Solomon parity per block on create (0 = off)
    pub cache_bytes: usize,        // page cache cap (default 64 MiB)
}
// builders: .create_if_missing(b) .with_key(k) .compress(b) .ecc(n) .cache_bytes(n)

pub struct Key([u8; 32]);          // raw key; Drop => zeroize. Deriving it from a
                                   // passphrase (a KDF) is the caller's responsibility.

#[non_exhaustive]
pub enum Error { Io(..), Corrupt{..}, ChainBroken{at: Version, ..}, WrongKey,
                 Sql{msg: String, pos: usize}, Conflict(MergeConflicts),
                 BranchNotFound(String), VersionNotFound(AsOf), Busy, .. }
pub type Result<T> = std::result::Result<T, Error>;

Dues coses per fixar-s’hi de bon principi. Value és un conjunt tancat de sis tipus SQL, de manera que no hi ha cap sorpresa de tipatge dinàmic. I els errors són un enum tipat i #[non_exhaustive]: una cadena de hashos trencada, una clau incorrecta o una corrupció al disc són valors que gestiones, mai dades errònies retornades en silenci com si res.

Database — cicle de vida, branques, auditoria

La Database és un handle barat i clonable cap al fitxer. Clonar-lo és només incrementar un Arc; comparteix-lo entre fils sense por.

impl Database {
    pub fn open(path: impl AsRef<Path>, opts: Options) -> Result<Database>;

    pub fn connect(&self) -> Result<Connection>;                  // "main" branch
    pub fn connect_branch(&self, name: &str) -> Result<Connection>;

    pub fn create_branch(&self, name: &str, from: AsOf) -> Result<()>;
    pub fn drop_branch(&self, name: &str) -> Result<()>;          // deletes the ref, not the pages
    pub fn branches(&self) -> Result<Vec<BranchInfo>>;            // {name, head: Version, created}

    pub fn diff(&self, from: &str, to: &str) -> Result<Diff>;     // O(changes), not O(data)
    pub fn merge(&self, from: &str, into: &str, policy: MergePolicy) -> Result<MergeReport>;

    pub fn verify(&self) -> Result<AuditReport>;                  // walks the entire hash chain
    pub fn verify_anchor(&self, anchor: &AuditAnchor) -> Result<AuditReport>; // detects truncation/rewriting

    pub fn history(&self) -> Result<Vec<Revision>>;               // "git log": timeline of versions
    pub fn diff_versions(&self, from: u64, to: u64) -> Result<Diff>;          // "git diff" between versions

    pub fn vacuum(&self, retention: Retention) -> Result<VacuumReport>;       // compacts + atomic rename
    pub fn vacuum_rekey(&self, retention: Retention, new_key: Option<Key>)    // compacts and rotates the key
        -> Result<VacuumReport>;
}

pub enum MergePolicy { FailOnConflict }                 // v1; future: Theirs, Ours, resolver
pub enum Retention   { KeepAll, KeepLast(u64), KeepSince(SystemTime) }

pub struct Diff { pub tables: Vec<TableDiff> }          // additions/deletions/modifications by rowid + schema diffs
pub struct AuditReport { pub head: u64, pub commits: u64, pub chain_ok: bool, pub chain_hash: [u8; 32] }
pub struct AuditAnchor { pub version: u64, pub chain_hash: [u8; 32] }   // AuditReport::anchor() creates it
pub struct Revision { pub version: u64, pub timestamp: SystemTime, pub parent: u64 }
pub struct VacuumReport {
    pub kept_from: u64, pub head: u64, pub reclaimed_versions: u64,
    pub pages_before: u64, pub pages_after: u64,
}

branches, diff i merge són la superfície de control de versions: una branca és un punter amb nom cap a una versió, diff és O(canvis) perquè recorre les dues arrels de commit i no pas les dades, i merge reprodueix una branca sobre una altra segons una política. verify() recorre tota la cadena de hashos i et diu que està intacta; verify_anchor() la compara amb una àncora que vas desar abans, que és la manera de detectar que t’han truncat o reescrit l’historial per sota. vacuum(Retention) és l’única operació que oblida: descarta les versions fora de la finestra de retenció i reescriu el conjunt viu en un fitxer nou amb un canvi de nom atòmic.

Connection — SQL, transaccions, viatge en el temps

impl Connection {
    pub fn execute(&self, sql: &str, params: &[Value]) -> Result<usize>;   // rows affected
    pub fn query(&self, sql: &str, params: &[Value]) -> Result<Rows>;
    pub fn prepare(&self, sql: &str) -> Result<Statement>;                 // parses once

    /// Bulk load: all rows in ONE transaction (one commit, one fdatasync),
    /// without a per-row SQL executor; index entries are inserted in bulk
    /// (UNIQUE verified). Autocommit only: either the whole batch or nothing.
    pub fn bulk_insert<I, R>(&self, table: &str, rows: I) -> Result<usize>
    where I: IntoIterator<Item = R>, R: AsRef<[Value]>;

    pub fn begin(&self) -> Result<Transaction<'_>>;     // acquires the single writer

    pub fn snapshot(&self, at: AsOf) -> Result<Connection>;  // pinned READ-ONLY connection
    pub fn version(&self) -> Version;                   // current head of the branch
    pub fn branch(&self) -> &str;
}

impl Transaction<'_> {
    pub fn execute(&self, sql: &str, params: &[Value]) -> Result<usize>;
    pub fn query(&self, sql: &str, params: &[Value]) -> Result<Rows>;      // reads its own writes
    pub fn commit(self) -> Result<Version>;
    pub fn rollback(self) -> Result<()>;                // Drop without commit => implicit rollback
}

execute/query fora d’una transacció són autocommit — una transacció per sentència. Dins d’un begin(), les sentències s’acumulen i commit() retorna la nova Version que han produït.

Un SELECT senzill — una projecció de columnes o un * sense WHERE/JOIN/agregat/ORDER BY — se serveix en mode streaming: Rows és propietari del seu snapshot i descodifica cada fila a mesura que itera, tocant només les columnes projectades i sense materialitzar mai el resultat sencer. Qualsevol altra consulta passa per l’executor complet; el resultat és indistingible tret del cost.

Snapshots i viatge en el temps

snapshot(at) et dóna una Connection de només lectura fixada a una versió passada. Com que l’historial és append-only, aquell snapshot és estable per més commits que hi arribin després — els lectors fixen una versió, l’escriptor únic n’afegeix de noves, i els dos no competeixen mai.

historial de commits (només afegir) v1 v2 v3 v4 v5 cap l'escriptor afegeix snapshot(Version(2)) lector @ cap
Un lector fixat conserva la seva versió encara que l'escriptor continuï afegint. Viatjar en el temps no és una còpia: és un segon punter al mateix historial immutable.

API del motor (sense SQL)

Accés tipat a les files que se salta l’analitzador SQL, el planificador i l’executor i va directament al catàleg i al b-tree — preservant totes les garanties (versionat, índexs, xifratge i cadena d’auditoria). Connection::table pren un snapshot consistent en crear-se; és una lectura estable que no veu les transaccions obertes. Escriure a nivell de motor es fa amb bulk_insert.

impl Connection {
    pub fn table(&self, name: &str) -> Result<TableReader>;  // snapshot on creation
}

impl TableReader {
    pub fn get(&self, rowid: i64) -> Result<Option<Vec<Value>>>;      // point lookup by PK
    pub fn scan(&self) -> Result<impl Iterator<Item = Result<(i64, Vec<Value>)>>>;
    pub fn scan_columns(&self, cols: &[usize]) -> Result<ProjectedScan>; // projected, no alloc/row
    pub fn count(&self) -> Result<u64>;
    pub fn column_index(&self, name: &str) -> Option<usize>;
    pub fn version(&self) -> u64;
}

impl ProjectedScan { // borrowing iterator: next() borrows &[Value] until the next call
    pub fn next(&mut self) -> Result<Option<&[Value]>>;
}

Per a què serveix: rendiment i control quan no cal SQL — llibres major encastats, magatzems d’esdeveniments, manteniment d’índexs a mida. Mesurat sobre el mateix motor i les mateixes dades, una cerca puntual per PK és unes 3,7× més ràpida que un SELECT … WHERE id = ?, perquè s’estalvia l’anàlisi, la planificació i la validació de cada crida. Un escaneig complet només hi guanya un 1,1×, perquè el camí d’escaneig d’SQL ja fa servir la mateixa ruta de streaming projectat — aquí el cost és la descodificació del registre de cada fila, no pas la capa SQL. En resum, l’avantatge de l’API del motor està en l’accés aleatori i puntual, no en l’escaneig.

Files i paràmetres

impl Rows { /* Iterator<Item = Result<Row>> */ }

impl Row {
    pub fn get<T: FromValue>(&self, col: impl ColIndex) -> Result<T>;  // by index or name
}

// FromValue for: i64, f64, String, Vec<u8>, bool, Option<T>, Value
// Into<Value> for the same, via a convenience macro:
let n = conn.execute(
    "INSERT INTO clients (name, created) VALUES (?1, ?2)",
    &params!["Acme GmbH", 1718000000_i64],
)?;

Exemple complet

use arkeion::{Database, Options, AsOf, MergePolicy, params};

let db = Database::open("tenant-42.arkeion", Options::default().with_key(key))?;
let conn = db.connect()?;

conn.execute("CREATE TABLE invoices (id INTEGER PRIMARY KEY, total REAL, status TEXT)", &[])?;

let tx = conn.begin()?;
tx.execute("INSERT INTO invoices (total, status) VALUES (?1, ?2)", &params![120.0, "draft"])?;
let v1 = tx.commit()?;

// — time-travel —
conn.execute("UPDATE invoices SET status = 'issued' WHERE id = 1", &[])?;
let before = conn.snapshot(AsOf::Version(v1.0))?;
let status: String = before.query("SELECT status FROM invoices WHERE id = 1", &[])?
                           .next().unwrap()?.get(0)?;          // "draft"

// — branching for a migration —
db.create_branch("vat-migration", AsOf::Head)?;
let mig = db.connect_branch("vat-migration")?;
mig.execute("UPDATE invoices SET total = total * 1.21", &[])?;
let diff = db.diff("main", "vat-migration")?;                  // review before merging
db.merge("vat-migration", "main", MergePolicy::FailOnConflict)?;

// — audit —
assert!(db.verify()?.chain_ok);

Garanties de l’API

  • Database: Send + Sync i barat de clonar; una Connection és per fil (Send, no Sync).
  • Les lectures no bloquegen mai cap escriptura, i cap escriptura no les bloqueja.
  • Escriptor únic. Les escriptures se serialitzen. L’autocommit (execute o bulk_insert fora d’una transacció) fa cua: sota contenció espera el seu torn — que dura microsegons, perquè el commit allibera l’escriptor abans del seu fdatasync i els syncs dels commits concurrents s’agrupen amb el group commit — i només retorna Busy si l’escriptor continua ocupat passat un llindar de seguretat. Una transacció explícita (begin() / BEGIN), en canvi, retorna Busy immediatament si hi ha una altra escriptura en curs, ja que pot retenir l’escriptor durant un temps indefinit i no ha de deixar penjats els altres cridadors.
  • snapshot() i les connexions de branca comparteixen la mateixa caché de pàgines de la Database.
  • Cada lectura valida la seva etiqueta d’autenticació; Corrupt, ChainBroken i WrongKey són errors tipats, mai dades errònies en silenci.

Accés per xarxa

L’API anterior és encastada i dins del mateix procés. Per a l’accés per xarxa, la capa client / servidor l’exposa sobre un protocol natiu — una branca per sessió, amb AS OF i verify com a operacions de primera classe — en crates separats:

  • arkeion-client (Client, MIT/Apache, repositori a part): connect, use_branch, execute, query / query_as_of, verify. Emmiralla el subconjunt de Connection que viatja per la connexió.
  • arkeiond (el servidor, propietari): el dimoni, amb un fil per connexió, que serveix cada sessió sobre una branca de la mateixa Database.

El protocol està fet a mà, sense serde, reaprofitant la codificació de varints i de Value del mateix motor.