El crate és arkeion a crates.io — la versió actual és la v0.12.
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.
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)",
¶ms!["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)", ¶ms![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 + Synci barat de clonar; unaConnectionés per fil (Send, noSync).- Les lectures no bloquegen mai cap escriptura, i cap escriptura no les bloqueja.
- Escriptor únic. Les escriptures se serialitzen. L’autocommit (
executeobulk_insertfora d’una transacció) fa cua: sota contenció espera el seu torn — que dura microsegons, perquè el commit allibera l’escriptor abans del seufdatasynci els syncs dels commits concurrents s’agrupen amb el group commit — i només retornaBusysi l’escriptor continua ocupat passat un llindar de seguretat. Una transacció explícita (begin()/BEGIN), en canvi, retornaBusyimmediatament 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 laDatabase.- Cada lectura valida la seva etiqueta d’autenticació;
Corrupt,ChainBrokeniWrongKeysó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 deConnectionque viatja per la connexió.arkeiond(el servidor, propietari): el dimoni, amb un fil per connexió, que serveix cada sessió sobre una branca de la mateixaDatabase.
El protocol està fet a mà, sense serde, reaprofitant la codificació de varints i de Value del mateix motor.