--- 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 headers; vector types; vector cells; int rows; int cols; }` - `struct Join { string alias; string source; vector> on; JoinStrategy strategy; vector fields; }` en `data_table_logic.h`. - `State.joins: vector` (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* 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.