Convencions: els enters de capçalera són little-endian; les claus del B-tree fan servir una codificació memcomparable big-endian (de manera que l’ordre de bytes coincideix amb l’ordre de claus). L’extensió de fitxer recomanada és .arkeion, però no és obligatòria — el motor obre qualsevol camí, sense configuració.

Disposició global

Una base de dades és un sol fitxer de pàgines fixes de 4096 bytes. Només tres regions són especials; tota la resta s’afegeix al final i no se sobreescriu mai.

db.arkeion — un fitxer · pàgines de 4096 bytes pàgina 0 — capçalera del fitxer immutable un cop creada pàgina 1 — ranura meta A pàgina 2 — ranura meta B les úniques escriptures in situ — punters d'arrencada alterns (una memòria cau de l'últim commit) pàgines 3… — zona de només afegir pàgines de dades · pàgines de commit — només s'afegeixen, mai no es reescriuen creix →
Les dues ranures meta són els únics bytes que se sobreescriuen, i no contenen res més que punters. Les dades i l'historial són de només afegir.
page 0        File header      (immutable after creation)
page 1        Meta slot A      (alternating rewrite, LMDB-style)
page 2        Meta slot B
page 3…       Append-only zone: data pages and commit pages

Pàgina en disc — 4096 bytes

PAGE_SIZE = 4096 coincideix amb una pàgina d’SO i un sector lògic habituals, cosa que minimitza les escriptures esquinçades i alinea la unitat de xifratge. PageId = u64, i l’offset en bytes d’una pàgina és simplement id × 4096.

Cada pàgina té la mateixa disposició de tres bandes, xifrada o no — la reserva criptogràfica (28 B, 0.7%) es paga sempre, a canvi d’un B-tree completament agnòstic respecte del xifratge.

cada pàgina — 4096 bytes nonce 12 B etiqueta 16 B cos 4068 B — xifrat o en clar 0 12 28 4096 l'etiqueta lliga el cos al seu page_id — una pàgina moguda a un altre offset es llegeix com a corrupció
Xifrada o no, els 4096 bytes queden coberts. La integritat es verifica en cada lectura.
0..12    nonce   encrypted: 96-bit GCM counter        · plaintext: zeros
12..28   tag     encrypted: GCM tag, AAD = page_id    · plaintext: SHA-256(LE(page_id) ‖ body)[0..16]
28..4096 body    encrypted: ciphertext of the body    · plaintext: the body as-is

El tag lliga el contingut al seu page_id: una pàgina reubicada a un altre offset es detecta com a corrupció, no s’accepta com a dades vàlides al lloc equivocat. Sense xifratge, el nonce ha de ser zero (es comprova). La integritat es verifica sempre en llegir, en tots dos modes. Amb el xifratge activat, totes les pàgines es xifren excepte la capçalera i els slots meta — i aquests no contenen dades d’usuari, només magics, punters i versions (ni tan sols els noms de taula queden en text clar).

Identificació de pàgines

Les pàgines de dades porten el tipus al primer byte del cos:

Tipus Valor Contingut
Fulla de B-tree 0x01 Cel·les clau→valor
Node intern de B-tree 0x02 Cel·les clau→fill
Overflow 0x03 Continuació de valors grans

Les pàgines estructurals s’identifiquen per un magic de 8 bytes a body[0..8] — no hi pot haver col·lisió, perquè cap byte de tipus de dades no és igual a 0x41 ('A'):

Pàgina Magic Posició
Capçalera "ARKEION1" pàgina 0
Slot meta "ARKMETA1" pàgines 1–2
Commit "ARKCMT01" zona d’append

Capçalera del fitxer — cos de la pàgina 0

0..8     magic            "ARKEION1"
8..12    format_version   u32 = 1
12..16   page_size        u32 = 4096
16..20   flags            u32   bit0 = encryption enabled
20..36   file_id          [16]  random at creation (seed of the genesis chain)
36..52   kdf_salt         [16]  reserved (raw 32-byte key today, no KDF)
52..     reserved         zeros

Slot meta — cos de les pàgines 1 i 2

0..8     magic              "ARKMETA1"
8..16    version            u64   version of the last commit reflected
16..24   last_commit_page   u64
24..32   n_pages            u64   file length (in pages) at that commit

L’escriptor alterna A/B. En obrir, el motor agafa el slot vàlid (integritat del tag) amb la version més alta, i després fa un escaneig cap endavant des de n_pages per si han aterrat commits després de l’última actualització del slot — l’actualització del slot és lazy, mantinguda deliberadament fora del camí crític de durabilitat.

Pàgina de commit — cos, tipus 0x04

0..8      magic           "ARKCMT01"
8..12     flags           u32   bit0 = compaction checkpoint (vacuum)
12..16    reserved        u32
16..24    version         u64   global monotonic, starts at 1
24..32    parent_page     u64   parent commit ON THE BRANCH (0 = genesis)
32..40    prev_page       u64   previous commit GLOBALLY (0 = genesis)
40..48    timestamp_ms    u64   wall clock, informational (version is authoritative)
48..56    data_root       u64   root of the branch's data tree
56..64    meta_root       u64   root of the global meta tree
64..72    nonce_counter   u64   next GCM counter after this commit
72..80    pages_written   u64   pages added by this commit
80..144   branch          [64]  UTF-8 branch name, zero-padded (max 64 B)
144..176  content_hash    [32]  SHA-256 of the PLAINTEXT bodies written by the commit, in order
176..208  prev_chain      [32]  chain_hash of the previous global commit
208..240  chain_hash      [32]  see below
240..     reserved        zeros

Cada commit tanca una hash chain sobre l’ordre global de tots els commits, a través de totes les branques:

cadena de hashes — lineal sobre l'ordre global de totes les branques commit v41 content_hash dels cossos en clar chain_hash commit v42 content_hash chain_hash commit v43 content_hash chain_hash · cap prev_chain prev_chain reescriu qualsevol commit passat i canvien tots els chain_hash posteriors — verify() ho comprova d'extrem a extrem
La cadena és lineal sobre l'ordre global dels commits. Com que content_hash cobreix el text en clar, un auditor verifica amb la clau a la mà — i la cadena sobreviu a la rotació de claus mitjançant vacuum.
chain_hash = SHA-256( prev_chain ‖ content_hash ‖ LE(version) ‖ LE(timestamp_ms)
                      ‖ LE(data_root) ‖ LE(meta_root) ‖ branch )
genesis:     prev_chain = SHA-256( "ARKEION1" ‖ file_id )

Manipular qualsevol escriptura passada, en qualsevol branca, trenca la cadena d’aquell punt en endavant. Un commit amb flags.checkpoint = 1 l’escriu vacuum: el seu prev_chain porta el chain_hash de cap de la història truncada, de manera que la cadena es manté contínua i verificable encara que les pàgines antigues ja no hi siguin.

Pàgines del B-tree

leaf (0x01):     [type u8][flags u8][ncells u16] · [plen varint][prefix]? · cells · pointers
                 flags bit0 = COMPRESSED PREFIX: a [plen varint][prefix] shared by every key
                   follows the header, and each cell stores only the SUFFIX key[plen..];
                   full key = prefix ++ suffix. Without the bit: empty prefix, cell = whole key.
                 cell: [cell-flags u8][klen|slen varint][key|suffix][vlen varint][val]
                 cell-flags bit0 = overflow: [...][total len varint][first overflow u64]
                 (a value pushing the cell past 1280 B goes to overflow; key max 1024 B)
internal (0x02): [type][flags][ncells u16][rightmost u64] · cells: [klen varint][key][child u64]
                 cell.key = exclusive upper bound of its child
overflow (0x03): [type][flags][len u16][next u64][bytes…]

Cada node acaba amb un array de punters de cel·la — un u16 LE per cel·la, que creix des del final del node cap al contingut — de manera que el descens, get i els escanejos fan cerca binària dins de pàgina sense materialitzar el node, i afegir la clau màxima es manté O(1). Cost: 2 B per cel·la.

Compressió de prefixos a les fulles. Les claus d’una fulla solen compartir un prefix llarg — postings d’un mateix terme ([0x03, fts_id, 0x00, term_id]), entrades d’un mateix índex ([0x02, index_id]). Emmagatzemar aquest prefix un cop per pàgina en lloc d’un cop per cel·la encongeix tots els índexs (és una part important del motiu pel qual el full-text va quedar per sota de l’FTS5 de SQLite), sense cap canvi al model lògic: les comparacions del hot path retallen el prefix de l’objectiu i comparen sufixos zero-copy; només el cursor d’escaneig reconstrueix la clau completa. El cursor d’append afegeix un sufix quan la clau comparteix el prefix, de manera que la fulla activa més a la dreta es manté comprimida amb appends O(1), i les pàgines més antigues es llegeixen de la mateixa manera.

Invariants comprovats en descodificar: les claus no són buides i són estrictament creixents dins d’un node. delete no reequilibra els nodes mig buits — només elimina els buits; vacuum reequilibra en reescriure.

Espais de claus

El catàleg emmagatzema una versió d’esquema amb cada taula. L’esquema lògic ha crescut v7 (restriccions CHECK) → v8 (full-text) → v9 (vectors) → v10 (re-rank vectorial) — cada pas afegeix espais de claus, de manera que el format_version en disc (pàgina 0) es manté a 1. Aquesta és la progressió canònica a què fan referència els docs de SQL, full-text i vectors.

Arbre de dades (data_root — es bifurca amb la branca)

[0x00, 0x01, table_name UTF-8]              → {table_id u32, schema}   catalogue
[0x00, 0x02, table_id BE]                   → next_rowid u64           rowid counter
[enc_oint(table_id), enc_oint(rowid)]       → record (row)
[0x02, index_id BE, value*, rowid u64 BE]   → secondary index entry

enc_oint és un i64 de longitud variable, autodelimitat i que preserva l’ordre: una capçalera d’un byte codifica el signe i el nombre de bytes significatius, de manera que les magnituds petites ocupen ~2 B mentre l’ordre (table_id, rowid) del B-tree es preserva exactament. La clau de fila no necessita cap byte d’espai de noms — table_id ≥ 1 dona una capçalera enc_oint de 0x80+, disjunta de 0x00 (catàleg) i 0x02 (índex), així que una clau s’autoidentifica pel seu primer byte. Les entrades d’índex mantenen un rowid u64 BE fix com a sufix final després del valor de longitud variable.

L’esquema viu a l’arbre de dades a propòsit: una migració en una branca canvia l’esquema només en aquella branca.

Arbre meta (meta_root — global, lineal, no es bifurca mai)

[0x01, branch_name]                → {head_version u64, head_page u64}    refs
[0x02, version BE]                 → {commit_page u64, ts_ms u64, branch}  history index
[0x03, ts_ms BE, version BE]       → ∅                                     AS OF TIMESTAMP

Registre (fila)

[ncols varint][tag u8 × ncols][payloads in order]

tags:  0 NULL · 1 FALSE · 2 TRUE · 3 INTEGER (zigzag varint)
       4 REAL (f64 LE, 8 B) · 5 TEXT (varint len + UTF-8) · 6 BLOB (varint len + bytes)

Les columnes finals absents es llegeixen com a NULL — que és el que permet que ALTER TABLE ADD COLUMN funcioni sense reescriure ni una sola fila existent.

Protocol de commit i recuperació

Una escriptura, des de l’únic escriptor:

1. new pages (copy-on-write)  → append
2. commit page                → append
3. fdatasync                  ← the durability point — exactly one
4. alternating meta slot      → write in place (lazy hint; no fsync)

Un fdatasync per commit, sense cap barrera entre les pàgines de dades i la pàgina de commit. El tag d’integritat per pàgina fa il·legible qualsevol pàgina escrita parcialment o no escrita; la recuperació escaneja en ordre i s’atura a la primera pàgina il·legible. Com que les pàgines de dades s’afegeixen abans que la pàgina de commit, un commit escrit a mitges no s’adopta mai — només un commit amb totes les pàgines llegibles (el fdatasync es va completar) esdevé el head. Estalviar-se la segona barrera val aproximadament 2–2.5× la taxa d’escriptures durables respecte d’un rollback journal de SQLite, a l’alçada del WAL, mantenint la durabilitat completa en un sol fitxer.

Obertura i recuperació:

1. Validate the header (magic, format version, flags).
2. Read meta A and B → candidate = the valid one with the highest version. Both corrupt → full scan.
3. Validate the candidate commit page (tag + magic + optional chain).
4. Scan forward from n_pages: each further valid commit page advances the head.
5. A broken tail (pages no valid commit covers) is ignored.

El pas 5 és el “replay del WAL”: no hi ha redo ni undo, només es descarta el que mai no es va confirmar.

Vacuum (compactació)

vacuum(retention) reescriu les versions vives en un fitxer temporal segons la política — KeepAll | KeepLast(u64) | KeepSince(SystemTime) — escriu un commit de checkpoint que s’encadena a la història truncada, i després fa un fsync + rename atòmics sobre l’original. Fa alhora de mecanisme de rotació de claus: tot es reescriu sota la clau nova.