feat(0133-1+2): columnar snapshot + string interning in data_table

Change 1 — Columnar Snapshot Internal:
- Add ColumnSnapshot struct (type + str_ids/i64/f64 per column) in data_table_internal.h
- Add SnapshotCache struct with pointer-identity sentinel (last_cells_ptr)
- Add SnapshotCache field to UiState singleton
- In render(): rebuild snapshot after join materialization when cells ptr changes
  Uses same pointer-identity pattern as existing stats_last_cells in State
  Int/Float columns parsed once via parse_number; String/Auto interned

Change 2 — String Interning:
- Add StringPool struct (strings + unordered_map<string_view, uint32_t>) to data_table_types.h
- StringPool is per-State (NOT global) for table isolation
- intern(sv) inserts if absent, returns stable uint32_t index
- Cleared + rebuilt on each snapshot rebuild for index coherence
- Add string_pool field to State struct

Documentation:
- Extended header comment in data_table_internal.h describing design,
  StringPool API, invariants (pointer-identity, row→snapshot_row),
  and how stats_last_cells and snapshot coexist independently

Build: fn_module_data_table + tables_qa pass, no new errors (only
pre-existing -Wformat-truncation warnings unrelated to this change).
Public API (data_table.h, TableInput, render() signature) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:35:12 +02:00
parent 6aa874f2b6
commit ea5c94fc8a
3 changed files with 199 additions and 0 deletions
+45
View File
@@ -8,6 +8,8 @@
#include "compute_column_stats.h"
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -353,6 +355,44 @@ struct VizPanel {
mutable ViewMode last_non_table = ViewMode::Bar;
};
// ----------------------------------------------------------------------------
// StringPool — interning de strings para columnas de texto (issue 0133).
// Una instancia por State (NOT global) para aislar tablas independientes.
//
// intern(sv) devuelve un indice uint32_t estable para la vida del rebuild.
// El pool se limpia (clear()) al inicio de cada rebuild de snapshot columnar.
//
// Invariante de invalidacion de string_view:
// - El vector `strings` se reserva con reserve() ANTES del primer intern()
// para evitar reallocs que invalidarian los string_view del mapa.
// Si la estimacion es insuficiente (columna con mas unicos de lo esperado),
// el mapa se reconstruye post-push_back: intern() verifica cap antes de
// insertar en el map para cubrir este caso.
// ----------------------------------------------------------------------------
struct StringPool {
std::vector<std::string> strings; // strings unicos, por indice
std::unordered_map<std::string_view, uint32_t> index; // sv→id (sv apunta a strings[i])
void clear() {
strings.clear();
index.clear();
}
// intern: inserta si no existe. Devuelve indice estable.
uint32_t intern(std::string_view sv) {
auto it = index.find(sv);
if (it != index.end()) return it->second;
uint32_t id = (uint32_t)strings.size();
strings.emplace_back(sv);
// Re-apuntar el string_view al almacenamiento interno (strings[id]).
index.emplace(std::string_view(strings[id]), id);
return id;
}
const std::string& at(uint32_t id) const { return strings[id]; }
bool empty() const { return strings.empty(); }
};
// ----------------------------------------------------------------------------
// State: stage pipeline + viz globales.
// ----------------------------------------------------------------------------
@@ -419,6 +459,11 @@ struct State {
std::vector<DrillStep> drill_back;
std::vector<DrillStep> drill_forward;
// String interning pool (issue 0133, Change 2).
// Limpiado y repoblado en cada rebuild del snapshot columnar.
// NOT global — una instancia por State para aislar tablas independientes.
StringPool string_pool;
// Helpers (definidos en compute_stage.cpp).
Stage& raw();
const Stage& raw() const;
+69
View File
@@ -816,6 +816,75 @@ void render(const char* id,
ensure_init(st, eff_cols);
auto& U = ui();
// -------------------------------------------------------------------------
// Issue 0133 — Change 1+2: Columnar snapshot + string interning.
//
// Se reconstruye si:
// - Es el primer frame (last_cells_ptr == nullptr), o
// - El puntero de `cells` cambio (caller reemplazo el buffer).
//
// Snapshot cubre las columnas ORIGINALES (pre-derived) del stage-0 input.
// Las derived columns no se incluyen en el snapshot — se calculan en
// compute_stage y el snapshot solo optimiza el acceso a datos crudos.
//
// StringPool.clear() + rebuild siempre que el snapshot se reconstruya,
// para mantener coherencia de indices entre pool y snapshot.
// -------------------------------------------------------------------------
if (U.snapshot.last_cells_ptr != cells) {
// Invalidar y reconstruir.
U.snapshot.last_cells_ptr = cells;
U.snapshot.cols.clear();
U.snapshot.cols.resize((size_t)orig_cols);
// Limpiar el StringPool del State para este rebuild.
st.string_pool.clear();
// Reservar capacidad estimada para evitar reallocs que invalidarian
// los string_view del mapa interno del pool.
// Estimamos hasta row_count valores unicos por columna string (worst case).
// En practica muchos menos; reserve no aloca el doble automatico.
st.string_pool.strings.reserve((size_t)(row_count < 65536 ? row_count : 65536));
for (int c = 0; c < orig_cols; ++c) {
ColumnSnapshot& cs = U.snapshot.cols[(size_t)c];
// Detectar tipo efectivo para esta columna.
ColumnType d = declared_types ? declared_types[c] : ColumnType::Auto;
ColumnType ct = effective_type(d, cells, row_count, orig_cols, c);
cs.type = ct;
if (ct == ColumnType::Int) {
cs.i64.resize((size_t)row_count);
for (int r = 0; r < row_count; ++r) {
const char* sv = cells[(size_t)(r * orig_cols + c)];
double tmp = 0.0;
if (sv && parse_number(sv, tmp)) {
cs.i64[(size_t)r] = (int64_t)tmp;
} else {
cs.i64[(size_t)r] = 0;
}
}
} else if (ct == ColumnType::Float) {
cs.f64.resize((size_t)row_count);
for (int r = 0; r < row_count; ++r) {
const char* sv = cells[(size_t)(r * orig_cols + c)];
double tmp = 0.0;
if (sv && parse_number(sv, tmp)) {
cs.f64[(size_t)r] = tmp;
} else {
cs.f64[(size_t)r] = 0.0;
}
}
} else {
// String, Bool, Date, Json, Auto → intern as string.
cs.str_ids.resize((size_t)row_count);
for (int r = 0; r < row_count; ++r) {
const char* sv = cells[(size_t)(r * orig_cols + c)];
std::string_view svv = sv ? std::string_view(sv) : std::string_view("");
cs.str_ids[(size_t)r] = st.string_pool.intern(svv);
}
}
}
}
// Build eff_headers / src_for_eff / eff_types para STAGE 0.
std::vector<const char*> eff_headers(eff_cols);
std::vector<int> src_for_eff(eff_cols);
+85
View File
@@ -4,6 +4,7 @@
// Este header lo incluyen SOLO los .cpp del modulo (data_table.cpp + sus 6 sub-funciones).
//
// Issue 0107c — split de data_table.cpp (4777 LOC) en 6 sub-funciones del registry.
// Issue 0133 — columnar snapshot + string interning (Changes 1+2).
//
// Provee:
// 1. `UiState` agregador (composicion de sub-states declarados en los .h de
@@ -14,6 +15,7 @@
// `effective_type`, `view_mode_label`, `join_strategy_label`, etc.
// 4. Forward refs de funciones internas que cruzan sub-funciones (ej. el
// draw_header_menu de chips llama draw_color_rule_menu de color_rules).
// 5. `ColumnSnapshot` / `SnapshotCache` — snapshot columnar interno (issue 0133).
//
// Politica:
// - Si un helper se usa SOLO dentro de UNA sub-funcion -> queda `static` en su .cpp.
@@ -22,6 +24,63 @@
//
// API publica externa = data_table/data_table.h (intacta tras refactor).
// API interna del modulo = este header.
//
// ---------------------------------------------------------------------------
// DISEÑO: Snapshot columnar + String Interning (issue 0133)
// ---------------------------------------------------------------------------
//
// MOTIVACION
// El layout de datos de entrada es row-major: `cells[row * cols + col]`
// (punteros a C-strings en el `TableInput` del caller). Acceder a una
// columna entera (para filtrar, ordenar, colorear, calcular stats) requiere
// saltar `cols` posiciones en memoria por fila — mal para cache a 10M+ filas.
// El snapshot convierte a column-major una sola vez por frame y amortiza el
// coste entre filter / sort / color_rules / stats.
//
// SNAPSHOT COLUMNAR — `ColumnSnapshot` / `SnapshotCache`
// - `SnapshotCache` vive en `UiState` (singleton thread_local de este modulo).
// - Contiene un vector de `ColumnSnapshot`, uno por columna efectiva de la
// tabla DESPUES de joins pero ANTES de stages (= stage-0 input).
// - `SnapshotCache::last_cells_ptr` guarda el puntero de `cells` del ultimo
// rebuild. Si el puntero cambia (o es la primera llamada), se reconstruye
// el snapshot completo. Esto sigue exactamente el patron de `stats_last_cells`
// en `State` (data_table_types.h:399).
// - String columns (ColumnType::String / Auto sin numero) almacenan indices
// uint32_t al `StringPool` del `State` correspondiente.
// - Int columns almacenan int64_t parseados una sola vez via `parse_number`.
// - Float columns almacenan double parseados una sola vez via `parse_number`.
// - Si `parse_number` falla en una celda que deberia ser Int o Float, la celda
// se trata como 0 / 0.0. Este comportamiento es consistente con `compare()`
// y el sort actual que ya llaman `parse_number` per-compare.
//
// STRING INTERNING — `StringPool` en `State`
// - `StringPool` vive en `State` (NOT global, NOT singleton) para que cada
// instancia de tabla tenga su propio pool sin interferencia.
// - `intern(sv)` inserta la cadena si no esta y devuelve su indice uint32_t.
// Usa `unordered_map<string_view, uint32_t>` con la `string_view` apuntando
// al `strings[i]` del vector (el vector se reserva antes del rebuild para
// evitar reallocs que invaliden los string_views del mapa).
// - En datasets tipicos (60-70% de strings repetidos) la reduccion de RAM
// es de 60-70% en la columna interned vs copias planas.
//
// INVARIANTES
// 1. Pointer-identity: si `cells == last_cells_ptr`, el snapshot es valido
// para este frame. Cambio de puntero => rebuild completo.
// 2. row→snapshot_row: el indice de fila en el snapshot es IDENTICO al indice
// de fila del `TableInput` original (no hay reordenacion en el snapshot).
// `TableEvent.row` del caller sigue siendo indice en `TableInput`.
// 3. El snapshot es input de stage-0. Los stages sucesivos (compute_stage)
// operan sobre `StageOutput` materializado y NO consultan el snapshot
// directamente — el snapshot solo alimenta la ruta stage-0 de render().
// 4. `stats_last_cells` y snapshot son independientes: `stats_last_cells`
// ya existia antes de issue 0133 y permanece como sentinel propio del
// cache de stats. El snapshot tiene su propio `last_cells_ptr`.
// Ambos pueden diferir temporalmente si `stats_cache` invalida por
// filtro (hash de filtros) pero el snapshot sigue valido por puntero.
// 5. `StringPool` se limpia (clear()) en cada rebuild del snapshot para
// mantener coherencia: los indices del snapshot siempre corresponden
// al pool del mismo frame.
// ---------------------------------------------------------------------------
#include "core/data_table_types.h"
#include "core/auto_detect_type.h"
@@ -40,6 +99,27 @@
namespace data_table {
// ---------------------------------------------------------------------------
// ColumnSnapshot — snapshot de una columna en memoria columnar.
// Creado una vez por frame al detectar cambio de puntero en `cells`.
// ---------------------------------------------------------------------------
struct ColumnSnapshot {
ColumnType type; // tipo efectivo inferido (post auto_detect)
std::vector<uint32_t> str_ids; // para String/Auto: indices al StringPool
std::vector<int64_t> i64; // para Int: valores parseados
std::vector<double> f64; // para Float: valores parseados
};
// ---------------------------------------------------------------------------
// SnapshotCache — vive en UiState (thread_local singleton).
// Un snapshot cubre TODAS las columnas efectivas de la tabla activa
// (post-join, pre-stages). Se invalida por pointer-identity de `cells`.
// ---------------------------------------------------------------------------
struct SnapshotCache {
const char* const* last_cells_ptr = nullptr; // sentinel de invalidacion
std::vector<ColumnSnapshot> cols; // un entry por columna efectiva
};
// ---------------------------------------------------------------------------
// UiState — singleton thread_local del modulo. Agrupa:
// (a) Sub-states declarados en headers de sub-funciones (AskAiState etc.).
@@ -120,6 +200,11 @@ struct UiState {
// ----- Export path (chips export action) -----
std::string last_export_path;
// ----- Columnar snapshot (issue 0133, Change 1) -----
// Invalida cuando cells pointer cambia entre frames.
// Usado en render() stage-0 path para filter/sort/color_rules/stats.
SnapshotCache snapshot;
};
// Singleton accessor. Definido en data_table.cpp (entrypoint).