La decisió central: el fitxer és el WAL
La majoria de bases de dades combinen un B-tree modificat in situ amb un write-ahead log separat: els canvis s’afegeixen primer al log i més tard es reintegren a l’arbre in situ. Arkeion fusiona aquestes dues estructures en una de sola. És un B-tree copy-on-write (CoW) que viu en un fitxer append-only. Una pàgina de dades, un cop escrita, no es modifica mai més. Cada transacció d’escriptura afegeix les pàgines noves i modificades al final del fitxer, seguides d’una petita pàgina de commit que registra les arrels de la nova versió.
Aquesta única decisió és tot el motor. Les quatre propietats que la gent sol afegir a posteriori en surten totes de franc:
| Propietat | Per què és automàtica |
|---|---|
| Time-travel | Les pàgines antigues són immutables, de manera que totes les versions passades continuen al disc. Llegir la versió N vol dir resoldre el commit N i llegir des de la seva arrel — O(log n), sense replay del log. |
| Ramificació | Una branca és només una ref amb nom que apunta a un commit, exactament com a git. Dues branques comparteixen físicament totes les pàgines que no han canviat. |
| Hash chain | Cada pàgina de commit porta el SHA-256 del seu propi contingut més el hash encadenat del commit anterior. La cadena de hashos existeix des del primer byte, no s’hi afegeix després. |
| Recuperació de fallades | Un commit només compta si la seva pàgina de commit és intacta. Després d’una fallada, una cua escrita a mitges simplement s’ignora. El “replay” és un escaneig cap endavant, sense res a desfer. |
| Lectures sense blocatge | Un lector fixa un commit i llegeix pàgines immutables. No es coordina mai amb l’escriptor. |
El preu és que el fitxer creix amb la seva història. Això es compensa amb vacuum, que compacta el fitxer segons una política de retenció (vegeu Format de fitxer).
Copy-on-write, pas a pas
Suposem un arbre petit amb una arrel que apunta a dues fulles, A i B, i que actualitzeu una fila que viu a B. Arkeion no toca B. Escriu una fulla nova B′ amb el canvi, després una arrel nova que apunta a l’A sense canvis i a la nova B′, i afegeix totes dues al final — seguides de la pàgina de commit de la nova versió. A és compartida per totes dues versions; B es queda al disc intacta. L’arrel antiga continua sent vàlida, així que la versió anterior es manté totalment llegible.
Capes i mòduls
El motor és una pila estricta: les dependències només apunten cap avall, i cap capa inferior no sap mai res d’una de superior. Això és el que fa que cada peça es pugui provar aïlladament.
| Mòdul | Responsabilitat | Tipus clau |
|---|---|---|
format |
Constants, nombres màgics, offsets de la disposició. Sense lògica. | PAGE_SIZE, PageId, PageType |
io |
Lectura/escriptura posicional portable (read_at a Unix, seek_read a Windows). |
DbFile |
crypto |
trait CryptoProvider { seal(page), open(page) }. Implementacions: Aes256GcmProvider, PlainProvider (integritat via SHA-256 truncat). |
CryptoProvider, Key |
pager |
Append de pàgines, lectures immutables amb cau (Arc<PageBuf>), slots meta A/B, validació d’integritat. |
Pager, PageBuf |
btree |
B-tree CoW: get / insert / delete / scan sobre claus de bytes, adreçat per PageId. Overflow per a valors grans. |
Tree, Cursor |
commit |
Construeix la pàgina de commit (arrels, hashos, comptador de nonce), el protocol de fdatasync, l’escaneig de recuperació, la verificació de la cadena. | CommitHeader, ChainVerifier |
tx |
Snapshot (una lectura, fixa un commit) i WriteTx (únic, serialitzat per un Mutex). |
Snapshot, WriteTx |
record |
Codificació de claus memcomparable i el format compacte de files. | Value, RowCodec, KeyCodec |
catalog |
Esquema de taules a l’arbre de dades (es ramifica amb les dades); refs i índex d’història a l’arbre meta (global). | Catalog, TableDef |
sql |
Lexer escrit a mà i parser de descens recursiu. Sense dependències. | Token, Stmt, Expr |
exec |
Planificador (full scan + filtre) i un executor iterador fila a fila. | Plan, Executor |
branch |
Diff entre branques (saltant-se els subarbres compartits físicament) i un merge a 3 bandes a nivell de fila. | Diff, MergeReport |
api |
La façana pública ergonòmica, a l’estil de rusqlite. L’únic mòdul re-exportat. | Database, Connection |
Els dos arbres
Cada commit fa referència a dues arrels, i es comporten de manera diferent a propòsit:
- Arbre de dades (
data_root) — el catàleg i les files. Segueix la seva branca: un commit en una branca es construeix sobre l’arrel de dades anterior d’aquella branca, de manera que una migració o una escriptura en una branca és invisible per a les altres. - Arbre meta (
meta_root) — les refs de branques més l’índex d’història (versió → commit, marca de temps → versió). És global i lineal: cada commit, de qualsevol branca, es construeix sobre l’arbre meta del commit global anterior. Per això les refs i l’índex de versions són una única font de veritat compartida que no divergeix mai entre branques.
Mantenir l’esquema a l’arbre de dades és el que fa que la ramificació sigui honesta: com que l’esquema viatja amb la branca, podeu fer evolucionar la forma de les dades en una branca i fusionar-la deliberadament, en lloc que es filtri a totes les branques alhora.
Model de concurrència
- Lectors —
Connection::snapshot()fixa una pàgina de commit i llegeix només pàgines immutables a través d’una cau compartida. Qualsevol nombre de lectors corre de manera concurrent, sense que cap comparteixi bloquejos amb l’escriptor. L’aïllament és snapshot isolation. - Escriptor — exactament un, serialitzat per un
Mutex<Writer>a nivell deDatabase. Les escriptures són, per tant, serialitzables per construcció. Avui el rendiment està limitat a un únic fil escriptor; el group commit (agrupar fsyncs concurrents) és una optimització planificada, encara no publicada. - Multiprocés — s’agafa un advisory file lock a
open(), per a un únic procés escriptor. Arbitrar escriptors entre processos queda fora de l’abast de moment.
Flux d’escriptura
Una escriptura es converteix en pàgines a memòria, les afegeix al final, hi afegeix una pàgina de commit i assoleix la durabilitat amb un únic fdatasync:
execute("UPDATE …")
→ sql::parse → exec::plan
→ WriteTx: btree CoW produces new pages in memory
→ pager: append the new pages, then the commit page
→ commit: one fdatasync ← the single durability point
→ meta slot A/B updated (lazy — a boot hint, off the critical path)
Deliberadament no hi ha cap fsync entre les pàgines de dades i la pàgina de commit. Les etiquetes d’integritat per pàgina fan il·legible qualsevol pàgina esquinçada o absent, i la recuperació s’atura a la primera pàgina il·legible — així, un commit escrit a mitges simplement no s’adopta mai. Amb una barrera per commit n’hi ha prou.
Flux de lectura amb time-travel
Llegir el passat no és cap replay; és una cerca i un escaneig ordinari des d’una arrel antiga:
query("SELECT … AS OF VERSION 42")
→ meta tree: history[42] → PageId of commit 42
→ Snapshot{commit 42} → btree::scan from data_root(42)