---
id: "0078"
title: "tables playground — joins MBQL-style (fase 9)"
status: completado
type: feature
domain: []
scope: multi-app
priority: media
depends: []
blocks: []
related: []
created: 2026-05-12
updated: 2026-05-17
tags: []
---

## 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.
