Vista de alto nivel del motor, su flujo interno y las responsabilidades por módulo.
🧭 Visión general
gabysql está dividido en capas simples y explícitas:
- CLI para trabajo local
- engine SQL para parsear y ejecutar
- catálogo para descubrir tablas
- índice persistente por PK
- pager + WAL para persistencia
- server HTTP para exponer la base
phpgabyadmincomo cliente web del API
🔄 Flujo principal
graph LR
CLI["gabysql / gabysql-server"] --> PARSER["Parser SQL"]
PARSER --> ENGINE["Engine"]
ENGINE --> CATALOG["Catalog"]
ENGINE --> TREE["Indice persistente"]
CATALOG --> PAGER["Pager"]
TREE --> PAGER
PAGER --> WAL["WAL"]
PAGER --> DB["Archivo .db"]
WEB["phpgabyadmin"] --> API["HTTP JSON"]
API --> ENGINE
🖥️ Flujo por CLI
gabysql execabre elPager- inicia transacción
parse()divide y valida sentenciasEngineejecuta cadaStatement- si todo sale bien,
commit()escribe WAL y aplica páginas - si algo falla, hace rollback
🌐 Flujo por HTTP
gabysql-serveracepta la request- valida token si existe
- abre la DB correspondiente
- protege escritura con mutex cuando corresponde
- ejecuta SQL o consulta catálogo/filas
- serializa JSON y responde
🧩 Componentes
src/storage.rs
Responsable de:
- crear (sin sobrescribir) y abrir archivos
.db; exponecreate_forcepara reset explícito - adquirir un lock exclusivo cross-process sobre el
.dbconFile::try_lock()en cadacreate/open: dos procesosgabysqlapuntando al mismo archivo → el segundo falla rápido condatabase is locked by another process(ver ADR-0013) - mantener el header del formato
VERSION=33(corte vigente al 2026-06-15; último bump P4 = column stats / 2026-06-10; bumps 14→33 extendieron triggers/procedures/functions/types extendidos/security/stats persistidas; rechaza explícitamente versiones anteriores; ver TECHNICAL_SPECS.md) - gestionar páginas (4096 bytes, los últimos 4 son trailer CRC32-IEEE)
- finalizar el checksum antes de cada flush y verificarlo al leer
- escribir WAL after-image, validar el CRC del payload de cada record y aplicar replay si hay marcador
COMMIT PageCachecon capacidad fija + LRU clean-only: cap defaultDEFAULT_CACHE_PAGES = 1024(~4 MB con páginas de 4 KB);Pager::set_cache_capacity(n)para tunear. Las páginas dirty nunca se evictan (correctness > strict cap; drenan en commit). Ver ADR-0009.
src/bptree.rs
B+Tree real con dos tipos de página:
LEAF: entradas(key, value)ordenadas, encadenadas pornextpara scans secuenciales eficientes.INTERNAL:(first_child, [(key, child) ...]). Lookup desciende por la rama correcta hasta llegar a una hoja.
Responsable de:
- almacenar pares
key -> value - inserción/upsert/delete por PK con splits en cascada
- mantener
root_pageestable cuando el root necesita splittear (técnica copy-up: el contenido del root se copia a una página nueva y el slot de root se reescribe como nuevoINTERNAL) - recorrer rangos y full scans descendiendo al leftmost-leaf y siguiendo el chain
next LeafCursor<'a>lazy que implementaIterator<Item = DbResult<KeyValue>>: carga páginas leaf on-demand vía la chainnexty short-circuita conIterator::take(n). HabilitaSELECT … LIMIT Nen O(N + offset) páginas leídas, no O(filas_totales). Ver ADR-0008. Desde ADR-0016,load_currentademás warm-ea la siguiente hoja enPageCache.
src/catalog.rs
Responsable de:
- registrar tablas usando hashing FNV-1a-64 (estable entre versiones de Rust)
- leer schema y resolver páginas raíz de cada tabla
- validar
CREATE TABLE(PK obligatoria, escalarINTo compuesta(a, b, ...)all-INT NOT NULL desde K2; identificadores[A-Za-z_][A-Za-z0-9_]*, ≤ 64 chars, no reservados) - persistir
Column { name, type, not_null, default?, references? }con flags por bit (0x01NOT NULL,0x02HAS_DEFAULT,0x04HAS_FK) — VERSION 5+ - persistir
TableMeta.primary_key_extra: Vec<String>(vacío para PK single, K2/VERSION 8), la lista deIndexMeta { name, column, root_page, unique, kind, extra_columns: Vec<String> }y la lista deCheckConstraint { name?, source_sql }(L2/VERSION 10, ADR-0021). Desde VERSION 11 cada constraint nombrada (PK/UNIQUE/FK/CHECK) puede llevar unname: Option<String>opcional usado porALTER TABLE DROP CONSTRAINT. - persistir
ForeignKeyMeta { table, column, on_delete, on_update, extra_source_columns: Vec<String>, extra_target_columns: Vec<String> }: las acciones aceptanRESTRICT/CASCADE/SET NULL/SET DEFAULT/NO ACTIONdesde L1/VERSION 9; losextra_*_columnsno vacíos implican FK multi-col desde residual #3/VERSION 12 (lookup via fingerprint K2). - validar FK targets al DDL (target table existe o es self-ref, target column o tupla de columnas es la PK del target, tipos coinciden)
- desde VERSION 13 (bloque V, ADR-0025) cada record del catalog lleva un discriminator byte (
0x01 = table,0x02 = view) y el catalog persiste tambiénViewMeta { name, source_sql, column_aliases }para vistas lógicas. Tablas y vistas comparten namespace ([GBY-4077]en colisión). - exponer
insert_row,upsert_row,delete_row,get_row,scan_rows,range_rows,remove_table,create_view,drop_view
src/index.rs
Responsable de los índices secundarios:
hash_value(FNV-1a-64 fijado, distinto del catálogo solo por dominio de uso, mismo algoritmo)encode_column_value— representación canónica del valor (NULL =[0], valor presente =[1] + bytes_del_tipo)- codec del bucket:
[count:u16] + N × ([vlen:u16][value][pk:i64]) bucket_insert / bucket_remove / bucket_lookupcon semántica idempotente para multivalorbucket_unique_conflict— usado por el path UNIQUE para detectar colisiones (NULL no colisiona)validate_indexable— rechaza columnasJSON(sin semántica canónica de igualdad)
src/sql.rs
Responsable de:
- tokenizar SQL
- parsear
CREATE TABLE(con constraints inline),DROP TABLE,ALTER TABLE ADD COLUMN,INSERT,SELECT(conORDER BY),UPDATE,DELETE,CREATE [UNIQUE] INDEX,DROP INDEX,INTEGRITY CHECK - validar tipos y filtros del
WHERE(WhereExprcon átomosEq/Compare/Between/Like/IsNull/InList/In(SELECT)/EqSubquery/Exists/EqColumnRefcombinados conAnd/Or/Not; precedencia estándar SQL; lógica trivaluada para NULL) - serializar y deserializar filas (con tolerancia a EOF para columnas trailing ausentes — habilita
ALTER ADD COLUMNsin reescritura) - ejecutar las sentencias contra el
Engine:INSERTaplica DEFAULTs, valida NOT NULL, pre-check de UNIQUE y FK antes de tocar disco; mantiene índices secundarios.UPDATEre-codifica la fila, valida NOT NULL/UNIQUE/FK sobre las columnas cambiadas, rechaza mutar la PK.DELETEresuelve cascade/restrict (delete_with_cascadecon worklist + visited set para cycles); mantiene índices.CREATE [UNIQUE] INDEXhace backfill antes de publicar el índice; UNIQUE aborta en duplicados.INTEGRITY CHECKbarre páginas (CRC), filas (decode), entradas de índice (PK existe), FKs (parent existe).
src/server.rs
Responsable de:
- exponer endpoints HTTP/JSON (incluido
GET /metricsconrequests_total,requests_by_status,errors_total, latencias p50/p95 y uptime; ver ADR-0014) - emitir logs estructurados a stdout cuando se arranca con
-log-json(una línea JSON por request:{ts_unix, method, path, status, latency_ms}) - resolver single DB o multi DB
- aplicar autenticación por token
- limitar conexiones simultáneas (default 64, configurable con
-max-connections); las que exceden el techo reciben503 - interceptar
CREATE DATABASE/DROP DATABASE/SHOW DATABASESen/execantes de abrir cualquierPager(esos statements no operan sobreTableMetasino sobre el directorio configurado con-dir); rechazar mezclarlos con sentencias de tabla en el mismo/exec - serializar resultados
web/phpgabyadmin/index.php
Responsable de:
- consultar el API HTTP
- ejecutar SQL desde navegador
- listar DBs, tablas y filas
- importar CSV vía múltiples
INSERT
🧠 Decisiones actuales
- Rust para el core del motor
- archivo único
.db+.waltemporal - SQL pequeño pero verificable
- server HTTP sin dependencias externas grandes
- admin web desacoplado del motor
⚖️ Trade-offs conscientes
- simplicidad por sobre throughput máximo
- claridad del storage por sobre feature breadth
- rango y full scan aceptables para tablas pequeñas o medianas
- seguridad básica suficiente para entorno controlado, no endurecimiento enterprise total
📈 Camino de evolución
Las mejoras naturales siguientes son:
✅ entregado (por PK)UPDATEyDELETEchecksums por página✅ entregado (CRC32-IEEE en trailer)B+Tree multinivel real✅ entregado (LEAF + INTERNAL con root estable)índices secundarios (una columna, equality)✅ entregado✅ entregado (VERSION 5)NOT NULL/DEFAULT/UNIQUEdeclarativos✅ entregadoDROP TABLE+ALTER TABLE ADD COLUMN✅ entregado (VERSION 6)FOREIGN KEYdeclarativas + enforced (RESTRICT / CASCADE)✅ entregadoINTEGRITY CHECKoperacional✅ entregadoORDER BY <col> [ASC|DESC]crash tests dirigidos (kill -9 entre WAL y file flush)✅ entregado✅ entregado (ADR-0008)LeafCursorlazy iterator (SELECT LIMIT en O(N+offset))✅ entregado (ADR-0009)PageCacheLRU acotado (memoria del server bounded)lock exclusivo cross-process del✅ entregado (ADR-0013).dblogs JSON + endpoint✅ entregado (ADR-0014)/metrics✅ entregado (ADR-0015)backup/restore/verifyCLI con CRC end-to-endprefetch en✅ entregado (ADR-0016)LeafCursor(warm-up de la próxima hoja)índice INT-ordenado + range scan por índice (VERSION 7)✅ entregado (ADR-0017)Subqueries (✅ entregadoIN (SELECT …),= (SELECT …)escalar,[NOT] EXISTSno-correlacionada/correlacionada single-eq)✅ entregadoJOIN(INNER, CROSS, comma-syntax, aliases, multi-tabla, self-join, LEFT/RIGHT/FULL [OUTER], USING, NATURAL, index-loop optimization)Transaction(Unit of Work) con cache deTableMeta— pendiente, ROI marginal hoy- WAL persistente estilo SQLite-WAL — diseño documentado, sin código (ver ADR-0018); condiciones de salida: bottleneck medido en gabybench, workload write-heavy real, o necesidad de MVCC
- índices compuestos all-INT ✅ (K2, 2026-05-26)
- range scan por índice secundario sobre
TEXT/FLOAT/DATE/DATETIME(pendiente — solo INT con OrderedInt) - planner cost-based real (hoy: deterministic dispatch + plan enum cerrado + index-loop join automático para INNER/LEFT con PK/índice + P3 stats session-scoped en EXPLAIN; P5 pendiente — reorden de joins por costo, choice de índice por costo)
- window functions ✅ (W3, 2026-05-29; W4 O(n) per partition, 2026-05-30), CTE no-rec ✅ (W1), CTE recursivas ✅ (W2, fixpoint con guard 10K; anchor bare-SELECT vía E5 — 2026-05-30), vistas ✅ (V, 2026-05-27; agregados sobre vista OK desde F2 — 2026-05-30) — materialized views con REFRESH pendientes. Agregados single-table desde F + sobre
SELECT con JOINdesde F2 (ADR-0066 Gap 1+7); único residualCOUNT(DISTINCT col)sobre JOIN. - subqueries correlacionadas con múltiples predicados (
AND/ORenWHEREinterno) ✅ (H, 2026-05-26) y derived tables (FROM (SELECT ...) t) ✅ (H, 2026-05-26) - política formal de migración entre versiones del formato en disco (sigue pendiente — cada bump rechaza versiones anteriores con
[GBY-1003]; backup + dump + recreate manual)