El modelo relacional ha dominado el almacenamiento de datos durante cincuenta años. Tablas, filas, columnas, claves foráneas, joins. Es una abstracción potente -- y una correspondencia terrible con la forma en que los desarrolladores realmente piensan sobre sus aplicaciones.
Cuando un desarrollador piensa en un blog, piensa: "Tengo Usuarios y Publicaciones. Un Usuario escribe muchas Publicaciones. Una Publicación tiene un título, cuerpo y fecha de publicación." No piensa: "Necesito una tabla users con columnas id INTEGER PRIMARY KEY, name VARCHAR(255), y una tabla posts con una clave foránea user_id referenciando users(id)."
El modelo relacional obliga a los desarrolladores a traducir su modelo mental -- entidades con propiedades y relaciones -- a un modelo mecánico -- tablas con columnas y claves de join. Esta traducción es donde vive la mayor parte de la complejidad accidental en el desarrollo de aplicaciones.
FlinDB elimina la traducción por completo. Piensas en entidades. Escribes en entidades. FlinDB almacena entidades. No hay desajuste de impedancia porque no hay desajuste.
La diferencia fundamental
En SQL, defines una tabla -- un contenedor para filas. Cada fila es un conjunto sin tipos de columnas. La tabla no tiene comportamiento. No sabe lo que representa. Es simplemente una cuadrícula de datos.
sqlCREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);En FLIN, defines una entidad -- un concepto de primera clase en el lenguaje. La entidad conoce sus campos, sus tipos, sus valores por defecto, sus restricciones y sus relaciones con otras entidades.
flinentity User {
name: text
email: text @unique
age: int?
}Estos parecen superficialmente similares. La diferencia es lo que pasa después.
Cuando agregas una fila a una tabla SQL, obtienes una fila. No tiene identidad más allá de su clave primaria. No tiene historial. No tiene comportamiento. No sabe que email debería ser único a menos que revises las definiciones de restricciones por separado.
Cuando guardas una entidad en FlinDB, obtienes un objeto con un ciclo de vida rico:
flinuser = User { name: "Thales", email: "[email protected]" }
save user
// user now has:
user.id // 1 (auto-assigned)
user.created_at // 2026-01-13T10:00:00Z
user.updated_at // 2026-01-13T10:00:00Z
user.version // 1
// Update creates a new version
user.name = "Juste Thales"
save user
user.version // 2
// History is always available
user.history // [version 1, version 2]
user @ -1 // The previous versionUna entidad no es una fila. Es un objeto versionado, con marca temporal, consciente de su historial, con una identidad que persiste a través de los cambios.
Tipos de campos: semánticos, no mecánicos
Los tipos de campos SQL describen formatos de almacenamiento. VARCHAR(255) significa "una cadena de hasta 255 caracteres". INTEGER significa "un número almacenado como entero de 64 bits". Estos tipos le dicen a la base de datos cómo almacenar bits en disco. No le dicen nada al desarrollador sobre lo que significan los datos.
Los tipos de campos de FLIN son semánticos. Describen lo que son los datos, no cómo se almacenan.
| Tipo | Descripción | Ejemplo |
|---|---|---|
text | Cadena UTF-8 | "Hello" |
int | Entero de 64 bits | 42 |
number | Flotante de 64 bits | 3.14 |
bool | Booleano | true |
time | Marca temporal UTC | now |
money | Valor monetario | 1000 CFA |
file | Referencia a archivo | Archivo subido |
semantic text | Texto con embeddings de IA | Contenido buscable por IA |
El tipo money es un buen ejemplo. En SQL, almacenarías un precio como DECIMAL(10,2) o INTEGER (centavos) y manejarías la conversión de moneda en tu código de aplicación. En FLIN, money es un tipo de primera clase que entiende moneda. 1000 CFA no es solo el número 1000 -- son mil francos CFA, y FlinDB conoce la diferencia.
El tipo semantic text es aún más notable. En SQL, si quieres búsqueda semántica potenciada por IA, necesitas instalar una extensión vectorial (pgvector), configurar un pipeline de embeddings, gestionar índices vectoriales y escribir consultas de similitud en una sintaxis personalizada. En FLIN, escribes:
flinentity Product {
name: text
description: semantic text
}Ese único modificador semantic le dice a FlinDB que genere automáticamente embeddings vectoriales para el campo description, los almacene en un índice vectorial interno y habilite la búsqueda por similitud. Sin extensión. Sin pipeline. Sin sintaxis personalizada.
Campos opcionales y valores por defecto
SQL maneja la nulabilidad con restricciones NULL y NOT NULL. Los valores por defecto se especifican con cláusulas DEFAULT. Ambos se definen a nivel de tabla, divorciados del significado de los datos.
FLIN maneja ambos a nivel de entidad con una sintaxis intuitiva:
flinentity Todo {
title: text
done: bool = false // Default: false
created: time = now // Default: current time
priority: int = 1 // Default: 1
notes: text? // Optional (can be none)
due_date: time? = none // Optional with explicit none
}El sufijo ? marca un campo como opcional. Los valores por defecto se especifican con =. Esto no es azúcar sintáctico sobre SQL -- es una forma fundamentalmente diferente de expresar requisitos de datos. Cuando lees una definición de entidad FLIN, entiendes el modelo de datos inmediatamente. Cuando lees una sentencia SQL CREATE TABLE, tienes que parsear mentalmente las definiciones de restricciones y las cláusulas de valores por defecto.
Relaciones: referencias, no claves foráneas
En el modelo relacional, las relaciones se expresan a través de claves foráneas -- una columna en una tabla que contiene la clave primaria de otra tabla. La base de datos no entiende que esto representa una relación. Solo sabe que una columna de enteros debe coincidir con valores en otra columna de enteros.
En FlinDB, las relaciones se expresan como referencias a entidades:
flinentity User {
name: text
}
entity Post {
title: text
body: text
author: User // Direct reference to a User entity
}El campo author: User no es una columna de clave foránea. Es una referencia tipada a una entidad User. FlinDB entiende esta relación a un nivel fundamental:
flin// Create entities
user = User { name: "Thales" }
save user
post = Post { title: "Hello World", body: "...", author: user }
save post
// Navigate the relationship
post.author // Returns the full User entity, not an ID
post.author.name // "Thales"
// Query through relationships
user_posts = Post.where(author == user)Sin joins. Sin cláusulas ON. Sin decisión entre LEFT OUTER JOIN y INNER JOIN. Referencias una entidad, y la referencia funciona. Consultas a través de una referencia, y la consulta funciona.
La implementación subyacente en Rust almacena las referencias eficientemente:
rustpub fn resolve_reference(
&self,
entity_type: &str,
entity_id: u64,
field_name: &str,
) -> DatabaseResult<Option<EntityInstance>>Cuando accedes a post.author, ZeroCore llama a resolve_reference("Post", post_id, "author"), que lee el ID de entidad almacenado del campo, busca la entidad User referenciada por tipo e ID (una operación O(1)), y devuelve la instancia completa de la entidad. El desarrollador nunca ve nada de esta maquinaria.
Muchos a muchos sin tablas intermedias
En SQL, las relaciones muchos a muchos requieren una tabla intermedia -- una tercera tabla que existe únicamente para conectar dos otras tablas:
sqlCREATE TABLE post_tags (
post_id INTEGER REFERENCES posts(id),
tag_id INTEGER REFERENCES tags(id),
PRIMARY KEY (post_id, tag_id)
);Esta tabla intermedia no tiene significado en el dominio de la aplicación. Es scaffolding puramente mecánico. En FlinDB, las relaciones muchos a muchos se expresan directamente:
flinentity Tag {
name: text
}
entity Post {
title: text
tags: [Tag] // List of Tag references
}
// Usage
post = Post {
title: "FlinDB Tutorial",
tags: [Tag.find(1), Tag.find(2)]
}
save post
// Query
post.tags // Returns list of Tag entitiesLa sintaxis [Tag] declara una lista de referencias a entidades. ZeroCore maneja el almacenamiento internamente, usando un formato de lista codificada:
rust// Lists are stored as encoded text for queryability
// Integer lists: __LIST__:1,2,3
// String lists: __LIST_STR__:admin,editor,viewerSin tabla intermedia. Sin consulta de join. Sin problema N+1 que resolver. La lista se almacena como parte de la entidad y se resuelve al acceder.
Auto-referencias y jerarquías
Los datos jerárquicos -- categorías, organigramas, hilos de comentarios -- son notoriamente difíciles en el modelo relacional. Terminas con CTEs recursivas, modelos de conjuntos anidados o columnas de ruta materializada. Cada enfoque tiene ventajas y desventajas y ninguno es intuitivo.
En FlinDB, las auto-referencias son naturales:
flinentity Category {
name: text
parent: Category? // Optional self-reference
}
// Build a tree
electronics = Category { name: "Electronics" }
save electronics
phones = Category { name: "Phones", parent: electronics }
save phones
smartphones = Category { name: "Smartphones", parent: phones }
save smartphonesEl campo parent: Category? es una referencia opcional al mismo tipo de entidad. ZeroCore maneja el almacenamiento y la recuperación, y el motor de consultas de grafos (implementado en la Sesión 166) proporciona operaciones de recorrido de árboles:
rust// Find path between entities
db.find_path("Category", root_id, leaf_id)?;
// Traverse relationships
db.traverse("Category", start_id, "parent", max_depth)?;
// Find connected components
db.connected_components("Category", "parent")?;Estas operaciones de grafos funcionan en cualquier entidad con referencias, no solo en auto-referencias. FlinDB trata todas las relaciones como aristas en un grafo, lo que significa que los mismos algoritmos de recorrido funcionan para cualquier patrón de relación.
El modelo de historial de versiones
La diferencia más fundamental entre FlinDB y una base de datos relacional es el versionado temporal. En SQL, una sentencia UPDATE destruye el valor anterior. Desaparecido. A menos que hayas construido manualmente un registro de auditoría, una tabla de historial o un sistema de event sourcing, los datos antiguos son irrecuperables.
En FlinDB, cada guardado crea una nueva versión:
flinuser = User { name: "Juste" }
save user // Version 1
user.name = "Juste G."
save user // Version 2
user.name = "Juste Gnimavo"
save user // Version 3Las tres versiones existen. Puedes acceder a cualquiera de ellas:
flinuser @ -1 // Version 2: "Juste G."
user @ -2 // Version 1: "Juste"
user @ "2026-01-13" // State at a specific date
user.history // All versionsEsto no es una funcionalidad añadida sobre un modelo relacional. Es el modelo de almacenamiento en sí. ZeroCore almacena las entidades como vectores de instancias versionadas:
rust// Data storage: entity_type -> id -> versions
data: HashMap<String, HashMap<EntityId, Vec<VersionedEntity>>>,La versión actual es el último elemento del vector. Las versiones anteriores son elementos anteriores. Las consultas temporales indexan directamente en este vector -- sin escaneos de tabla, sin subconsultas, sin funciones de ventana.
Delete vs Destroy: el modelo CRUDD
FlinDB introduce una distinción que SQL carece: la diferencia entre eliminar datos y destruirlos.
flinuser = User.find(1)
delete user // Soft delete: sets deleted_at, preserves historyflinuser = User.find(1)
destroy user // Hard delete: permanent removal, GDPR compliancedelete es suave -- marca la entidad con una marca temporal deleted_at pero preserva todo el historial de versiones. La entidad desaparece de las consultas pero puede recuperarse. Este es el predeterminado, porque la mayoría de las eliminaciones en aplicaciones reales son errores o estados temporales.
destroy es permanente -- elimina permanentemente la entidad y todo su historial. Esto es para cumplimiento (derecho al olvido del GDPR) o cuando genuinamente necesitas reclamar almacenamiento.
Ambas operaciones desencadenan comportamiento en cascada en entidades relacionadas. Y ambas están completamente soportadas en el motor Rust:
rustfn handle_on_delete_or_destroy(
&mut self,
entity_type: &str,
entity_id: u64,
is_destroy: bool,
) -> DatabaseResult<()> {
match behavior {
OnDeleteBehavior::Cascade => {
if is_destroy {
self.destroy(&ref_type, id)?;
} else {
self.delete(&ref_type, id)?;
}
}
OnDeleteBehavior::SetNull => { /* ... */ }
OnDeleteBehavior::Restrict => { /* ... */ }
}
}Por qué importa el diseño centrado en entidades
El diseño centrado en entidades no es solo conveniencia sintáctica. Cambia la relación del desarrollador con los datos.
En un modelo centrado en tablas, los datos son inertes. Están en filas. Operas sobre ellos con consultas externas. La tabla no sabe lo que representa, y el desarrollador debe mantener el mapeo entre el modelo de dominio de la aplicación y el modelo de almacenamiento en su cabeza.
En un modelo centrado en entidades, los datos están vivos. Tienen identidad, historial, relaciones y comportamiento. La definición de entidad es tanto el esquema como la documentación. Cuando lees entity User { name: text, email: text @unique }, entiendes todo sobre cómo funcionan los Usuarios en esta aplicación.
Esto es especialmente poderoso para la audiencia que FLIN sirve. Los estudiantes en Abiyán, Dakar y Lagos que aprenden a programar por primera vez no necesitan aprender SQL, ORMs, migraciones y restricciones de claves foráneas antes de poder construir su primera aplicación. Definen entidades, las guardan y las consultan. La base de datos desaparece detrás del lenguaje.
Y para los desarrolladores profesionales que construyen con FLIN, el modelo centrado en entidades elimina toda una categoría de bugs: los que surgen del desajuste entre el modelo de la aplicación y el modelo de almacenamiento. No hay desajuste porque son lo mismo.
Esta es la Parte 2 de la serie "Cómo construimos FlinDB", documentando cómo construimos un motor de base de datos embebido completo para el lenguaje de programación FLIN.
Navegación de la serie: - [056] FlinDB: Zero-Configuration Embedded Database - [057] Entities, Not Tables: How FlinDB Thinks About Data (estás aquí) - [058] CRUD Without SQL - [059] Constraints and Validation in FlinDB - [060] Aggregations and Analytics