Files
fn_registry/dev/issues/0078-tables-joins-mbql.md
T
egutierrez e3c8979e8d chore: auto-commit (95 archivos)
- cmd/fn/doctor.go
- cmd/fn/main.go
- cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt
- cpp/apps/primitives_gallery/playground/tables/data_table.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.h
- cpp/apps/primitives_gallery/playground/tables/self_test.cpp
- cpp/apps/primitives_gallery/playground/tables/tql.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.h
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:50:34 +02:00

130 lines
4.2 KiB
Markdown

---
id: 0078
title: tables playground — joins MBQL-style (fase 9)
status: done
priority: medium
created: 2026-05-12
closed: 2026-05-12
related_components: [cpp/apps/primitives_gallery/playground/tables, lua_engine, tql]
---
## Contexto
Fase 9 del roadmap del tables playground. Hoy `render()` acepta un solo input
table. MBQL permite `:joins` para combinar varias tablas en una sola query.
Queremos lo mismo en TQL.
Roadmap restante tras esta fase:
- 10: drill-through extendido
- 11: LLM API ("Ask AI")
- 12: promote a registry + migrar apps C++
## Diseño (referencia MBQL)
MBQL `:joins`:
```clojure
:joins [{:source-table 2
:alias "orders"
:condition [:= [:field "user_id"] [:field "user_id" {:join-alias "orders"}]]
:strategy :left-join
:fields :all}]
```
Adaptacion a nuestro modelo:
- Sin DB ids — usamos nombres de inputs pasados en runtime.
- Soportar multi-key composite via vector de pares.
- Strategies: `left` | `inner` | `right` | `full`.
- Fields: `all` | `none` | lista.
## Cambios
### Tipos / API
- `struct TableInput { string name; vector<string> headers; vector<ColumnType> types; vector<const char*> cells; int rows; int cols; }`
- `struct Join { string alias; string source; vector<pair<string,string>> on; JoinStrategy strategy; vector<string> fields; }` en `data_table_logic.h`.
- `State.joins: vector<Join>` (antes de stages[0]).
- `render()` signature extendida:
```cpp
void render(const char* id,
const char* const* headers, int col_count,
const char* const* cells, int row_count,
State& st,
const ColumnType* declared_types,
const std::vector<TableInput>* joinables = nullptr);
```
### Logica pura
- `join_tables_cpp_core(left, right, alias, on, strategy, fields) -> StageOutput`.
- Tests: left/inner/right/full join + multi-key + NULL handling (left propaga `""`).
- Pre-pipeline: si `state.joins` no vacio, materializar tabla joined (recorriendo joinables[]) ANTES de aplicar stage 0.
- Headers post-join prefijados: `alias.col` (preserva originales del main).
### UI
- **Chip row "Joins:"** debajo de "Filters:". + button abre popup con:
- Combo alias (auto-sugerido) + combo source (nombre de input)
- Strategy combo
- on[] list: par left-col / right-col con + para añadir mas pares (multi-key)
- Fields radio: all / none / pick
- Chip muestra `alias <- source on left=right (left-join)`. Right-click edit, X borrar.
- **CONDICIONAL**: la fila de joins solo se renderiza si `joinables != nullptr && !joinables->empty()`. Sin tablas extra → no UI de joins, no se pierde espacio en apps que solo pasan una tabla.
### TQL
Nuevo bloque root-level:
```lua
return {
version = 1,
display = "table",
joins = {
{alias = "orders", source = "orders_tbl",
on = { {"user_id", "user_id"} },
strategy = "left", fields = "all"},
},
stages = { ... },
columns = { ... },
views = { ... },
}
```
emit/apply round-trip + tests.
### Lua engine
YA aplicado preempt (2026-05-12): `ident_cont` acepta `.` para parsear
`[alias.col]` post-join sin colision.
### compute_stage / find_orig_col
No requieren cambios — operan sobre strings de col names. Aceptan `alias.col` directamente.
### extra_panels
No requieren cambios — ven el StageOutput post-join automaticamente.
## Pasos de implementacion
1. `join_tables_cpp_core` (pure) + tests unit.
2. `TableInput` struct + `Join` struct + `State.joins`.
3. `render()` signature extendida (default `nullptr`, back-compat).
4. Pre-pipeline materialize join cuando `state.joins` no vacio.
5. UI chip row (condicional a joinables disponibles).
6. TQL emit/apply joins + tests round-trip.
7. Lua `[alias.col]` resolver test (la sintaxis ya parsea).
8. Actualizar tests phase9: 4 join types + multi-key + NULL + TQL round-trip.
## No-objetivos (esta fase)
- Subqueries / source-query MBQL — no aplicable, las inputs ya estan materializadas.
- Joins recursivos — flat list, sin chains internos.
- Outer joins con conditions no-igualdad (`>`, `<`, range) — solo `=` por ahora.
## Riesgos
- Performance con tablas grandes — hash join sobre key cols. Para v1, nested loop
con hash map sobre right table es suficiente.
- Memoria — joined table puede ser N*M en worst case. Documentar.