Pipelines de Datos
El pipeline de reportes de Lana se ejecuta dentro de la aplicación. La base de datos operacional publica eventos públicos, el relé outbox los aterriza como lotes Parquet append-only en una zona de aterrizaje local, dbt los transforma sobre DuckDB, y los trabajos de reportes en Rust generan archivos de reporte descargables a partir de una copia de servicio publicada.
Descripción General
El pipeline proporciona:
- Tablas de almacén basadas en eventos para informes y análisis.
- Transformaciones dbt en
dw, ejecutándose sobre DuckDB. - Archivos de informes regulatorios y operativos generados por trabajos Rust en
core/report. - Comprobaciones CI que comparan esquemas de eventos públicos, modelos demux de dbt y definiciones de informes.
Almacén de Datos DuckDB
El almacén de datos es DuckDB — no hay un servidor de base de datos para el almacén. La aplicación necesita un único directorio local, configurado como dw_db.path (variable de entorno LANA__DW_DB__PATH). En Kubernetes, el despliegue monta el PVC lana-dw en /var/lib/lana/dw; los entornos locales de desarrollo lo apuntan a .nix-deps/dw en el worktree. Una ruta vacía deja el almacén sin configurar: el relé no aterriza nada, no se ejecutan construcciones y la generación de informes manuales y de fin de día queda deshabilitada para ese despliegue.
El directorio contiene todo el estado local del pipeline:
<dw root>/landing/events/events-<first>-<last>.parquet eventos públicos relayados
<dw root>/landing/party_pii/party-pii-<first>-<last>.parquet stream de PII de partes relayado
<dw root>/landing/_checkpoint.json prefijo de eventos confirmado
<dw root>/build/lana_dw.duckdb base de datos de construcción dbt
<dw root>/serve/lana_dw.duckdb copia de servicio publicada
Zona de Aterrizaje
El relé outbox (dentro de la aplicación lana) aterriza los eventos públicos y el stream de PII de partes como lotes Parquet append-only: un archivo por lote confirmado y por stream, nombrado por el rango de secuencias de commit que cubre. Después de cada lote, el relé renombra atómicamente _checkpoint.json, que nombra el prefijo de eventos confirmado — todo evento con una secuencia de commit hasta ese valor está presente de forma duradera en la zona de aterrizaje. Una caída entre la escritura de un lote y su checkpoint deja archivos Parquet huérfanos más allá del checkpoint; el siguiente flush elimina esos huérfanos antes de escribir, por lo que la repetición nunca duplica filas.
Por defecto, la zona de aterrizaje es un directorio local bajo la raíz del DW. Los despliegues de producción configuran un espejo en almacenamiento de objetos mediante la sección dw_db.landing:
dw_db:
path: /var/lib/lana/dw
landing:
provider: gcp
bucket: galoystaging-lana-bank-<instancia>-dw
prefix: dw
El directorio local sigue siendo la copia de trabajo — el relé y la construcción dbt solo la tocan a ella — mientras cada lote y checkpoint también se sube al bucket mediante la identidad de workload del pod (sin credenciales configuradas). Al iniciar el pod, una zona de aterrizaje local vacía se restaura desde el espejo antes de que el relé corra, por lo que la raíz del DW puede ser efímera (emptyDir): el despliegue no necesita PVC, y las bases de datos build/serve se reconstruyen con la corrida inicial del pipeline después de la restauración. El orden del flush mantiene al espejo como autoridad: archivos locales → subidas al objeto → checkpoint del objeto → checkpoint local, así el checkpoint del bucket nunca nombra datos que no tiene y una caída en cualquier punto se resuelve con la siguiente repetición.
Nota de despliegue: habilitar el espejo en una instancia que ya tiene historia aterrizada inicializa el bucket desde la zona local en el primer arranque — pero esa historia local debe existir aún en el momento del cambio. Migre una zona aterrizada respaldada por PVC (suba su contenido al bucket) antes de remover el volumen.
Construcción dbt
dbt se ejecuta únicamente sobre dbt-duckdb (perfil dbt_lana_duckdb_dw en dw/profiles.yml). Las fuentes leen la zona de aterrizaje mediante external locations, y la construcción escribe <dw root>/build/lana_dw.duckdb; el estado incremental persiste entre construcciones. El perfil toma ambas ubicaciones del entorno: LANA_DUCKDB_PATH (la base de datos de construcción) y LANA_DW_LANDING_DIR (la zona de aterrizaje).
Copia de Servicio
Después de cada construcción completa o intradía exitosa, la aplicación publica una copia de servicio atómica en <dw root>/serve/lana_dw.duckdb: hace checkpoint del WAL de la base de construcción, copia el archivo junto a la ruta de servicio y lo renombra en su lugar. La copia conserva el mismo nombre de archivo que la base de construcción para que las referencias calificadas por catálogo que dbt incorpora en vistas almacenadas y macros de reportes sigan resolviendo. Los trabajos de reportes y el endpoint de consultas de solo lectura abren la copia de servicio en modo solo lectura y nunca compiten con una construcción; los lectores nuevos toman una copia recién publicada en su siguiente conexión.
Transformaciones dbt
El proyecto dbt se encuentra en:
dw/
├── models/
│ ├── staging/
│ │ └── lana_events/
│ │ └── demux/
│ ├── intermediate/
│ │ ├── streams/
│ │ ├── reference/
│ │ ├── enriched/
│ │ ├── entities/
│ │ └── core/
│ └── outputs/
│ ├── marts/
│ └── reports/
├── macros/
├── seeds/
├── dbt_project.yml
└── profiles.yml
Los niveles del modelo son:
| Nivel | Propósito |
|---|---|
| Staging | Entradas conformadas al origen. Los eventos públicos se dividen en variantes tipadas generadas bajo staging/demux/ (stg_<module>_<variant>); solo los streams intermedios pueden leer esas variantes. Los datos PII se preparan por separado como stg_party_pii. |
| Intermediate | Transformaciones con un propósito específico. Los modelos en streams/ forman logs append-only por entidad (int_<entity>_events); reference/, enriched/ y entities/ conservan sus roles especializados. La lógica compartida de intervalos, balances y álgebra de conjuntos vive en core/; la lógica regulatoria específica de cada informe vive en las carpetas por norma. Ningún modelo vive directamente en la raíz de intermediate/ — cada modelo vive en una subcarpeta con nombre. |
| Outputs | Interfaces terminales para consumidores downstream. marts/ contiene relaciones conformadas fct_* / dim_* para analítica; reports/ contiene relaciones de informe finales y macros de reportes consumidas por el ejecutor de informes Rust. Cada una conserva su propio contrato y esquema. |
Los informes as-of utilizan la materialización as_of_sql_function, que construye una macro de tabla de DuckDB — report_x(date) — invocable desde SQL exactamente como las anteriores funciones SQL de Postgres, con la fecha as-of vinculada por el llamador.
Manejo de PII
El stream de aterrizaje party_pii es append-only y lleva marcadores de borrado (tombstones): una fila con payload NULL marca el PII de la parte como borrado. La instantánea de PII vigente se deriva en stg_party_pii — gana el último commit_sequence por parte, y un tombstone significa que ningún PII aparece downstream. Esa instantánea permanece como una vista no persistente, según las salvaguardas de PII del lint del dw.
Generación de Informes
Las descargas de informes consultan la copia de servicio publicada. El ejecutor de informes vincula las fechas y lee la copia de servicio en modo solo lectura a través del motor DuckDB embebido (el crate Rust duckdb), genera salidas en formato CSV, XML o texto a partir de la definición del informe en core/report/report-definitions/reports.yml, y carga el resultado al almacenamiento configurado. Descargar un informe no ejecuta dbt por sí mismo; las actualizaciones del almacén usan los disparadores del pipeline.
El endpoint de consultas ad-hoc de solo lectura valida el SQL enviado con sqlparser y luego lo ejecuta en modo solo lectura contra la copia de servicio con search_path mart,main y un límite de filas. Ya no hay un timeout de sentencia — el límite de filas es el único tope.
Verificaciones de CI
Las comprobaciones del pipeline de datos validan el proyecto dbt-duckdb sin provisionar infraestructura de almacén: dbt parse solo renderiza el proyecto, de modo que bastan ubicaciones DuckDB temporales.
dev/check-dw.pyutiliza el manifiesto de dbt para validar los límites de dependencias entre modelos, las dependencias directas del watermark de los streams, las convenciones de informes y marts, y la materialización de PII.dev/generate-dw-demux.py checkverifica que los modelos de staging de eventos públicos generados coincidan con el esquema y el manifiesto de bundle (dw/stream-events.yml).dev/generate-dw-streams.py checkverifica que los modelos de streams de eventos generados coincidan con el esquema y el manifiesto de bundle.int_gl_transaction_template_entriesincorpora la clasificación de reporte de cada entrada de plantilla (declarada como metadato de entrada de cala en el momento de definición de la plantilla y reflejada atómicamente enAccounting.LedgerTxTemplateCreated; las plantillas de cala son de creación única, así que publicar una vez es completo), por lo que una clasificación no puede desincronizarse entre el código y el warehouse.int_cala_classified_transaction_amountsderiva su mapeo de ese flujo, y una pruebadbt_utils.unique_combination_of_columnssobre las combinaciones clasificadas protege el join contra el conteo doble.ci/tasks/validate-report-columns.shanaliza el manifiesto de dbt y verifica que las definiciones de informes as-of apunten a modelosas_of_sql_functionutilizando la macro de filtro as-of esperada.