Formato en disco, WAL, tipos, límites y decisiones técnicas actuales del motor.


🧬 Identidad del formato

Campo Valor
Magic GABYSQL1
Versión de formato 13
Tamaño de página 4096 bytes (fijo en esta versión)
Trailer de checksum por página 4 bytes (CRC32-IEEE)
Hashing del catálogo y de claves de índice FNV-1a-64 (estable entre versiones de Rust)
Tipos de página B+Tree LEAF (1), INTERNAL (2)
Bumps de versión: 12 cambió el hash del catálogo de DefaultHasher a FNV-1a-64; 23 reservó el trailer CRC y agregó verificación en lectura/replay; 34 extendió TableMeta con la lista de índices secundarios; 45 agregó NOT NULL + DEFAULT por columna y el flag unique por índice; 56 agregó FOREIGN KEY opcional por columna (target table + target column + ON DELETE action); 67 agregó el campo kind: IndexKind (Hash OrderedInt) a IndexMeta para habilitar índices ordenados sobre columnas INT con range scan O(log N + k) — ver ADR-0017; 78 extendió TableMeta.primary_key y IndexMeta.column a múltiples columnas (PK e índices compuestos) — restringido a all-INT NOT NULL, equality lookup via fingerprint FNV-1a-64, ver ADR-0019; 89 (L1) extendió ForeignKeyMeta con on_delete ampliado (SET NULL / SET DEFAULT / NO ACTION) y nuevo on_update con las cinco acciones — ver ADR-0020; 910 (L2) agregó CHECK (expr) column-level y table-level persistidas como texto canónico vía format_expr — ver ADR-0021; 1011 (residual #2) agregó nombres opcionales para PK/UNIQUE/FK/CHECK (pk_name, fk_name, etc.) habilitando ALTER TABLE DROP CONSTRAINT — ver ADR-0022; 1112 (residual #3) agregó extra_source_columns + extra_target_columns a ForeignKeyMeta para FK multi-col FOREIGN KEY (a, b) REFERENCES p (x, y) con lookup O(log n) via fingerprint K2 — ver ADR-0023; 1213 (bloque V) agregó un discriminator byte por record del catalog (0x01 = table, 0x02 = view) y persiste ViewMeta { name, source_sql, column_aliases } para vistas lógicas — ver ADR-0025; 1314/15 (X3) agregó ObjectKind::Procedure con payload reescrito en el bump 15 — ver ADR-0031; 1516 (X3b) agregó ObjectKind::Function — ver ADR-0032; 1617 (Y) tipos TIME y UUID; 1718 (Y2) max_length: Option<u32> para enforcement VARCHAR(n)/CHAR(n) — ver ADR-0040; 1819 (Y3) int_width: Option<u8> para enforcement TINY/SMALL/MEDIUM/INT4 — ver ADR-0043; 1920 (Y4) ColumnType::Blob con literal X'hex' — ver ADR-0044; 2021 (Y5) high bit 0x80 en int_width = UNSIGNED — ver ADR-0045; 2122 (Y6) ColumnType::Decimal exacto (i128 + scale por fila + (precision, scale) por columna) — ver ADR-0046; 2223 (Z1) ObjectKind::User + ObjectKind::Role — ver ADR-0050; 2324 (Z2) ObjectKind::Grant (bitmask de privs por (grantee, object)) — ver ADR-0051; 2425 (Z3) ObjectKind::Policy con USING (expr) para RLS — ver ADR-0052; 2526 (Z1b) reescritura de UserMeta con scheme byte + salt + hash + iterations PBKDF2-SHA256 — ver ADR-0053; 2627 (Z3b) PolicyMeta agrega with_check_sql + acepta FOR INSERT — ver ADR-0054; 2728 (Z1c) scheme=2 scrypt RFC 7914 — ver ADR-0056; 2829 (Z1d) Blake2b RFC 7693 como cimiento — ver ADR-0058; 2930 (Z1e) Argon2id scheme=3 estructural — ver ADR-0060; 3031 (Z1f) corte semántico tras el partial fix de Argon2id — ver ADR-0061. Las DBs de versiones anteriores son rechazadas explícitamente al abrir con [GBY-1003]. Los bloques P1+P2+P3 (Fase 3, 2026-05-29) son zero-bump on-disk — las stats de ANALYZE son session-only en Engine.table_stats: HashMap.

📦 Header de la página 0

Offset Significado
0..7 magic
8..11 versión u32 little-endian (debe ser 13)
12..13 page size u16 little-endian (debe ser 4096)
16..19 page count u32 little-endian
20..23 catalog_root_page u32 little-endian
last 4 CRC32-IEEE del resto de la página

💾 Modelo de persistencia


♻️ WAL

Formato actual:

El payload bytes de cada record es la página completa de len = page_size, incluido su trailer CRC. Por tanto, el CRC de la página dentro del WAL hace de checksum del record: durante replay_to la verificación falla si el WAL fue truncado o flipped antes de aplicarse al .db.

Regla de durabilidad

  1. el Pager finaliza el CRC trailer de cada página dirty
  2. se escriben after-images al WAL
  3. se escribe COMMIT
  4. se sincroniza el WAL
  5. se aplican páginas al .db
  6. se sincroniza el .db
  7. se elimina el .wal

Recovery


🌿 Índice persistente actual

B+Tree real con dos tipos de página:

Tipo Layout
LEAF (1) [type:u8][next:u32][count:u16] + count × ([key:i64][vlen:u16][bytes])
INTERNAL (2) [type:u8][reserved:u32][count:u16][first_child:u32] + count × ([key:i64][child:u32])

🧠 Cache de páginas (Pager)

El Pager mantiene un PageCache con capacidad fija (default DEFAULT_CACHE_PAGES = 1024, configurable con Pager::set_cache_capacity). Política:

Implicancia para el server: memoria total acotada por cache_capacity × #DBs_abiertas × page_size. Default: 50 DBs × 1024 × 4 KB ≈ 200 MB. Predecible, no swappea, no OOM. Ver ADR-0009.


🚶 Cursor lazy sobre B+Tree

bptree::LeafCursor<'a> implementa Iterator<Item = DbResult<KeyValue>>:

Garantía de complejidad para SELECT … LIMIT N sin ORDER BY: O(N + offset) páginas leídas, no O(filas_totales). Ver ADR-0008.

Desde ADR-0016, LeafCursor::load_current hace page_data también sobre la siguiente hoja del chain — warm-ea la PageCache antes de que el caller la pida, sin allocations adicionales (la página queda en el cache LRU). Helper Pager::cache_contains(page_no) -> bool expuesto para introspección/tests.


Cada TableMeta contiene:

Layout binario v13 por record del catalog: cada entry comienza con un discriminator byte (0x01 = TableMeta, 0x02 = ViewMeta, bloque V/ADR-0025). Para tablas, el cuerpo es el TableMeta clásico extendido. Por columna: [name][type_code:u8][flags:u8] seguido del payload del default cuando flags & 0x02, y del payload del FK cuando flags & 0x04. Bits: 0x01 = NOT NULL, 0x02 = HAS_DEFAULT, 0x04 = HAS_FK. Cada TableMeta además persiste [pk_count:u8] columnas PK (≥1; >1 implica PK compuesta all-INT NOT NULL, K2), un pk_name: Option<String> (residual #2/V11) y cada IndexMeta persiste [extra_cols_count:u8] columnas extra (>0 implica índice compuesto all-INT) + name: Option<String>.

El catálogo direcciona por FNV-1a-64 del nombre normalizado (trim + lowercase). Una colisión de hash devuelve error explícito al abrir.


🔍 Índices secundarios

Desde VERSION 7 cada IndexMeta lleva un campo kind: IndexKind (Hash OrderedInt). Desde VERSION 8 (K2) además admite columnas extra para índices compuestos (extra_columns: Vec<String>, vacío para single-column):

Buckets (kind = Hash)

Un índice hash es un B+Tree paralelo cuya clave es el FNV-1a-64 del valor de la columna y cuyo valor es un bucket (lista) de (value_bytes, pk):

Campo Encoding
Bucket [count:u16] + count × ([vlen:u16][value_bytes][pk:i64])
value_bytes Representación canónica del valor (encode_column_value): NULL = [0], otros = [1] + bytes_específicos_del_tipo

Operaciones:

Restricciones de la versión actual:

Modo UNIQUE:


🔗 FOREIGN KEY (VERSION 6+)

Cada columna puede declarar como mucho una FK single-column. Se persiste en Column.references = Some(ForeignKeyMeta { table, column, on_delete }).

Reglas de validación al DDL (CREATE TABLE / ALTER ADD COLUMN):

Enforcement en runtime:


🧾 Tipos de columna


🧱 Reglas de fila


🧠 Gramática SQL soportada

Soportado

No soportado todavía


🌐 Semántica HTTP


⚠️ Limitaciones técnicas actuales


🧠 Qué significa esto en producto

gabysql ya tiene una base sólida para aprender, demostrar y estabilizar storage/SQL básicos, pero todavía no tiene las capas de optimizer, concurrencia y compatibilidad histórica que definen un motor maduro.