Files
primitives_gallery/playground/tables/tql.cpp
T
egutierrez 100aeaa1fc chore: auto-commit (12 archivos)
- playground/tables/CMakeLists.txt
- playground/tables/data_table.cpp
- playground/tables/data_table_logic.cpp
- playground/tables/data_table_logic.h
- playground/tables/self_test.cpp
- playground/tables/tql.cpp
- playground/tables/viz.cpp
- playground/tables/viz.h
- playground/tables/llm_anthropic.cpp
- playground/tables/llm_anthropic.h
- ...

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

914 lines
36 KiB
C++

#include "tql.h"
#include "lua_engine.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <unordered_map>
namespace tql {
using namespace data_table;
namespace {
int find_orig_col(const std::vector<std::string>& headers, const std::string& name) {
for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
return -1;
}
int find_derived_idx(const std::vector<DerivedColumn>& d, const std::string& name) {
for (size_t i = 0; i < d.size(); ++i) if (d[i].name == name) return (int)i;
return -1;
}
Op parse_op(const std::string& s) {
if (s == "=") return Op::Eq;
if (s == "!=") return Op::Neq;
if (s == ">") return Op::Gt;
if (s == ">=") return Op::Gte;
if (s == "<") return Op::Lt;
if (s == "<=") return Op::Lte;
if (s == "contains") return Op::Contains;
if (s == "!contains") return Op::NotContains;
if (s == "starts") return Op::StartsWith;
if (s == "ends") return Op::EndsWith;
return Op::Eq;
}
std::string lua_to_string(lua_State* L, int idx) {
if (lua_isnil(L, idx)) return "";
if (lua_isboolean(L, idx)) return lua_toboolean(L, idx) ? "true" : "false";
size_t n = 0;
const char* s = luaL_tolstring(L, idx, &n);
std::string out(s, n);
lua_pop(L, 1);
return out;
}
} // anon
std::string lua_string_literal(const std::string& s) {
std::string out;
out.reserve(s.size() + 4);
out += '"';
for (char c : s) {
switch (c) {
case '\\': out += "\\\\"; break;
case '"': out += "\\\""; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if ((unsigned char)c < 0x20) {
char b[8]; std::snprintf(b, sizeof(b), "\\%d", (unsigned char)c);
out += b;
} else out += c;
}
}
out += '"';
return out;
}
std::string color_to_hex(unsigned int c) {
unsigned int r = c & 0xFF;
unsigned int g = (c >> 8) & 0xFF;
unsigned int b = (c >> 16) & 0xFF;
unsigned int a = (c >> 24) & 0xFF;
char buf[16];
if (a == 0xFF) std::snprintf(buf, sizeof(buf), "#%02x%02x%02x", r, g, b);
else std::snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", r, g, b, a);
return buf;
}
unsigned int hex_to_color(const std::string& s) {
if (s.size() < 7 || s[0] != '#') return 0xFFFFFFFF;
auto hex2 = [&](size_t i) -> unsigned int {
unsigned int v = 0;
if (i + 1 < s.size()) std::sscanf(s.c_str() + i, "%2x", &v);
return v;
};
unsigned int r = hex2(1), g = hex2(3), b = hex2(5);
unsigned int a = (s.size() >= 9) ? hex2(7) : 0xFF;
return r | (g << 8) | (b << 16) | (a << 24);
}
ColumnType column_type_from_string(const std::string& s) {
if (s == "string") return ColumnType::String;
if (s == "int") return ColumnType::Int;
if (s == "float") return ColumnType::Float;
if (s == "bool") return ColumnType::Bool;
if (s == "date") return ColumnType::Date;
if (s == "json") return ColumnType::Json;
return ColumnType::Auto;
}
// Helper: header del Stage 0 dado un col idx eff. Para stages 1+ no aplica
// (los stage outputs tienen sus propios headers).
namespace {
const char* agg_fn_token(AggFn f) {
switch (f) {
case AggFn::Count: return "count";
case AggFn::Sum: return "sum";
case AggFn::Avg: return "avg";
case AggFn::Min: return "min";
case AggFn::Max: return "max";
case AggFn::Distinct: return "distinct";
case AggFn::Stddev: return "stddev";
case AggFn::Median: return "median";
case AggFn::P25: return "p25";
case AggFn::P75: return "p75";
case AggFn::P90: return "p90";
case AggFn::P99: return "p99";
case AggFn::Percentile: return "percentile";
}
return "?";
}
AggFn agg_fn_from_string(const std::string& s) {
if (s == "count") return AggFn::Count;
if (s == "sum") return AggFn::Sum;
if (s == "avg") return AggFn::Avg;
if (s == "min") return AggFn::Min;
if (s == "max") return AggFn::Max;
if (s == "distinct") return AggFn::Distinct;
if (s == "stddev") return AggFn::Stddev;
if (s == "median") return AggFn::Median;
if (s == "p25") return AggFn::P25;
if (s == "p75") return AggFn::P75;
if (s == "p90") return AggFn::P90;
if (s == "p99") return AggFn::P99;
if (s == "percentile") return AggFn::Percentile;
return AggFn::Count;
}
} // anon
std::string emit(const State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& types)
{
int orig_cols = (int)headers.size();
const Stage& raw = state.raw();
int eff_cols = orig_cols + (int)raw.derived.size();
// Build effective headers + types (same indexing as col_visible/order)
std::vector<std::string> eff_headers(eff_cols);
std::vector<ColumnType> eff_types(eff_cols);
for (int c = 0; c < orig_cols; ++c) {
eff_headers[c] = headers[c];
eff_types[c] = (c < (int)types.size()) ? types[c] : ColumnType::Auto;
}
for (int k = 0; k < (int)raw.derived.size(); ++k) {
eff_headers[orig_cols + k] = raw.derived[k].name;
eff_types[orig_cols + k] = raw.derived[k].type;
}
// Build order positions: col_idx -> visual order (1-based)
std::unordered_map<int, int> order_pos;
for (size_t i = 0; i < state.col_order.size(); ++i) {
order_pos[state.col_order[i]] = (int)i + 1;
}
auto emit_filter_block = [&](const std::vector<Filter>& filters,
const std::vector<std::string>& stage_headers,
const char* indent) -> std::string {
if (filters.empty()) return {};
std::string s;
s += indent; s += "filter = {\n";
for (const auto& f : filters) {
std::string col_name = (f.col >= 0 && f.col < (int)stage_headers.size())
? stage_headers[f.col] : "";
s += indent; s += " {";
s += lua_string_literal(op_label(f.op));
s += ", ";
s += lua_string_literal(col_name);
s += ", ";
s += lua_string_literal(f.value);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
auto emit_sort_block = [&](const std::vector<SortClause>& sorts,
const char* indent) -> std::string {
if (sorts.empty()) return {};
std::string s;
s += indent; s += "sort = {\n";
for (const auto& sc : sorts) {
s += indent; s += " {";
s += lua_string_literal(sc.desc ? "desc" : "asc");
s += ", ";
s += lua_string_literal(sc.col);
s += "},\n";
}
s += indent; s += "},\n";
return s;
};
std::string out;
out += "-- TQL v1 (Table Query Language). Round-trip de State <-> Lua.\n";
out += "-- Schema:\n";
out += "-- version = 1 -- bump si breaking change\n";
out += "-- display = \"table\" -- table|bar|line|pie (futuro)\n";
out += "-- stages = { stage0, stage1, ... } -- pipeline; stage 0 = Raw\n";
out += "-- columns = { {name,type,visible,order,color_rules}, ... }\n";
out += "--\n";
out += "-- Stage 0 (Raw): filter + expressions + sort\n";
out += "-- Stage N (Grouped): filter + breakout + aggregation + sort\n";
out += "--\n";
out += "-- filter: {{op, col, val}, ...} op in =,!=,>,>=,<,<=,contains,!contains,starts,ends\n";
out += "-- expressions: {[name] = \"lua_body\"} ej: [\"total\"] = \"return [a] + [b]\"\n";
out += "-- breakout: {\"col1\", \"col2\"} group by\n";
out += "-- aggregation: {{fn, col, arg?}, ...} fn in count,sum,avg,min,max,distinct,stddev,median,p25,p75,p90,p99,percentile\n";
out += "-- sort: {{dir, col}, ...} dir in asc,desc\n";
out += "return {\n";
out += " version = 1,\n";
out += " display = ";
out += lua_string_literal(view_mode_token(state.display));
out += ",\n";
if (!state.main_source.empty()) {
out += " main_source = ";
out += lua_string_literal(state.main_source);
out += ",\n";
}
// joins (antes de stages, materializa input)
if (!state.joins.empty()) {
out += " joins = {\n";
for (const auto& jn : state.joins) {
out += " {alias = " + lua_string_literal(jn.alias);
out += ", source = " + lua_string_literal(jn.source);
out += ", strategy = " + lua_string_literal(join_strategy_token(jn.strategy));
out += ", on = {";
for (size_t i = 0; i < jn.on.size(); ++i) {
if (i) out += ", ";
out += "{" + lua_string_literal(jn.on[i].first) + ", "
+ lua_string_literal(jn.on[i].second) + "}";
}
out += "}";
if (!jn.fields.empty()) {
out += ", fields = {";
for (size_t i = 0; i < jn.fields.size(); ++i) {
if (i) out += ", ";
out += lua_string_literal(jn.fields[i]);
}
out += "}";
}
out += "},\n";
}
out += " },\n";
}
out += " stages = {\n";
// Recorre todos los stages; stage 0 tiene formato Raw (filter+expr+sort),
// stages 1+ tienen formato Grouped (filter+breakout+aggregation+sort).
// Headers para resolver col indices de filters/sorts se computan stage por
// stage simulando la cadena.
std::vector<std::string> cur_headers = headers; // stage input headers
// Para stage 0 raw, los headers incluyen orig + derived.
// Construye cur_headers iniciales (= orig); derived se anaden al pasar stage 0.
for (int si = 0; si < (int)state.stages.size(); ++si) {
const Stage& stg = state.stages[si];
out += " {\n";
if (si == 0) {
// Stage 0: orig headers + derived seran disponibles tras expressions.
// Para los filter col indices, asumimos van con cur_headers = orig.
// (data_table.cpp solo aplica filters a orig cols al guardar; si en
// futuro stage 0 admite filter sobre derived, se traduce a name.)
std::vector<std::string> s0_headers = headers;
// Filters
out += emit_filter_block(stg.filters, s0_headers, " ");
// Expressions
if (!stg.derived.empty()) {
bool any = false;
for (const auto& d : stg.derived) if (!d.formula.empty()) { any = true; break; }
if (any) {
out += " expressions = {\n";
for (const auto& d : stg.derived) {
if (d.formula.empty()) continue;
out += " [";
out += lua_string_literal(d.name);
out += "] = ";
out += lua_string_literal(d.formula);
out += ",\n";
}
out += " },\n";
}
}
// Sort (sort.col es string en nuevo modelo).
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: orig + derived.
for (const auto& d : stg.derived) cur_headers.push_back(d.name);
} else {
// Stage 1+: filter (sobre output del previo, cur_headers).
out += emit_filter_block(stg.filters, cur_headers, " ");
// breakout
if (!stg.breakouts.empty()) {
out += " breakout = {";
for (size_t i = 0; i < stg.breakouts.size(); ++i) {
if (i > 0) out += ", ";
out += lua_string_literal(stg.breakouts[i]);
}
out += "},\n";
}
// aggregation
if (!stg.aggregations.empty()) {
out += " aggregation = {\n";
for (const auto& a : stg.aggregations) {
out += " {";
out += lua_string_literal(agg_fn_token(a.fn));
if (a.fn != AggFn::Count) {
out += ", ";
out += lua_string_literal(a.col);
}
if (a.fn == AggFn::Percentile) {
char buf[32]; std::snprintf(buf, sizeof(buf), "%g", a.arg);
out += ", "; out += buf;
}
out += "},\n";
}
out += " },\n";
}
// sort
out += emit_sort_block(stg.sorts, " ");
// Avanza cur_headers para siguiente stage: breakouts + agg aliases.
std::vector<std::string> next;
for (const auto& b : stg.breakouts) next.push_back(b);
for (const auto& a : stg.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
out += " },\n";
}
out += " },\n";
// columns (per-col render config) — siempre referidas a los effective cols
// del STAGE 0 (asumimos viz state para stage 0 / raw). Renderizar columns
// por cada stage no aporta v1.
out += " columns = {\n";
for (int c = 0; c < eff_cols; ++c) {
out += " {";
out += "name = " + lua_string_literal(eff_headers[c]);
out += ", type = " + lua_string_literal(column_type_name(eff_types[c]));
bool vis = (c < (int)state.col_visible.size()) ? state.col_visible[c] : true;
out += std::string(", visible = ") + (vis ? "true" : "false");
int order = order_pos.count(c) ? order_pos[c] : c + 1;
out += ", order = " + std::to_string(order);
// color rules for this col
bool first = true;
for (const auto& cr : state.color_rules) {
if (cr.col != c) continue;
if (first) { out += ", color_rules = {"; first = false; }
else { out += ", "; }
out += "{equals = " + lua_string_literal(cr.equals);
out += ", color = " + lua_string_literal(color_to_hex(cr.color)) + "}";
}
if (!first) out += "}";
out += "},\n";
}
out += " },\n";
// views (extra viz panels — viz adicional sobre mismos stages)
auto emit_view = [&](const VizPanel& p) -> std::string {
std::string s = " {";
s += "display = " + lua_string_literal(view_mode_token(p.display));
if (!p.config.x_col.empty()) s += ", x_col = " + lua_string_literal(p.config.x_col);
if (!p.config.cat_col.empty()) s += ", cat_col = " + lua_string_literal(p.config.cat_col);
if (!p.config.size_col.empty()) s += ", size_col = "+ lua_string_literal(p.config.size_col);
if (!p.config.y_cols.empty()) {
s += ", y_cols = {";
for (size_t i = 0; i < p.config.y_cols.size(); ++i) {
if (i) s += ", ";
s += lua_string_literal(p.config.y_cols[i]);
}
s += "}";
}
if (p.config.primary_color != 0)
s += ", color = " + lua_string_literal(color_to_hex(p.config.primary_color));
if (p.config.hist_bins > 0)
s += ", hist_bins = " + std::to_string(p.config.hist_bins);
if (p.config.pie_radius > 0)
s += ", pie_radius = " + std::to_string(p.config.pie_radius);
if (!p.config.show_legend) s += ", show_legend = false";
if (p.config.show_markers) s += ", show_markers = true";
if (p.config.locked) s += ", locked = true";
s += "},\n";
return s;
};
out += " views = {\n";
// Panel 0 = main viz
VizPanel main_p;
main_p.display = state.display;
main_p.config = state.viz_config;
out += emit_view(main_p);
for (const auto& p : state.extra_panels) out += emit_view(p);
out += " },\n";
out += " visualization_settings = {},\n";
out += "}\n";
return out;
}
bool apply(const std::string& lua_text, State& state,
const std::vector<std::string>& headers,
const std::vector<ColumnType>& /*types*/,
const char* const* cells, int rows, int orig_cols,
std::string* err)
{
std::vector<std::string> warns;
auto warn = [&](const std::string& m) { warns.push_back(m); };
auto finish_with_warns = [&](bool ok) -> bool {
if (err && !warns.empty()) {
std::string j;
for (size_t i = 0; i < warns.size(); ++i) {
if (i) j += "; ";
j += warns[i];
}
*err = j;
}
return ok;
};
lua_State* L = lua_engine::raw_state();
if (!L) { if (err) *err = "lua engine null"; return false; }
if (luaL_loadbufferx(L, lua_text.data(), lua_text.size(), "tql", "t") != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load error";
lua_pop(L, 1);
return false;
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
if (err) *err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "exec error";
lua_pop(L, 1);
return false;
}
if (!lua_istable(L, -1)) {
if (err) *err = "TQL root must be a table";
lua_pop(L, 1);
return false;
}
// main_source
lua_getfield(L, -1, "main_source");
if (lua_isstring(L, -1)) state.main_source = lua_tostring(L, -1);
else state.main_source.clear();
lua_pop(L, 1);
// display
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) {
std::string d = lua_tostring(L, -1);
ViewMode m = view_mode_from_token(d.c_str());
state.display = m;
if (d != "table" && std::strcmp(view_mode_token(m), d.c_str()) != 0) {
warn("unknown display \"" + d + "\" (defaulting to table)");
}
}
lua_pop(L, 1);
// Validar version.
lua_getfield(L, -1, "version");
if (lua_isnil(L, -1)) {
warn("version missing (assuming 1)");
} else if (!lua_isnumber(L, -1)) {
if (err) *err = "version must be a number";
lua_pop(L, 2);
return false;
} else {
int v = (int)lua_tointeger(L, -1);
if (v != 1) {
char buf[64]; std::snprintf(buf, sizeof(buf), "unsupported TQL version %d (expected 1)", v);
if (err) *err = buf;
lua_pop(L, 2);
return false;
}
}
lua_pop(L, 1);
// Reset partes mutables. Liberar lua_ids antes.
for (auto& s : state.stages) {
for (auto& d : s.derived) {
if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
}
}
state.stages.clear();
state.active_stage = 0;
state.color_rules.clear();
// ---- Walk joins[] ----
state.joins.clear();
lua_getfield(L, -1, "joins");
if (lua_istable(L, -1)) {
int nj = (int)lua_rawlen(L, -1);
for (int i = 1; i <= nj; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Join jn;
lua_getfield(L, -1, "alias");
if (lua_isstring(L, -1)) jn.alias = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "source");
if (lua_isstring(L, -1)) jn.source = lua_tostring(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "strategy");
if (lua_isstring(L, -1)) jn.strategy = join_strategy_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "on");
if (lua_istable(L, -1)) {
int on_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= on_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string a = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string b = lua_to_string(L, -1); lua_pop(L, 1);
jn.on.push_back({a, b});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "fields");
if (lua_istable(L, -1)) {
int fn_n = (int)lua_rawlen(L, -1);
for (int k = 1; k <= fn_n; ++k) {
lua_rawgeti(L, -1, k);
if (lua_isstring(L, -1)) jn.fields.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.joins.push_back(jn);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// ---- Walk stages[] ----
lua_getfield(L, -1, "stages");
if (lua_istable(L, -1)) {
int n_stages = (int)lua_rawlen(L, -1);
// Headers efectivos por stage para resolver filter/sort col indices.
std::vector<std::string> cur_headers = headers;
for (int si = 1; si <= n_stages; ++si) {
lua_rawgeti(L, -1, si);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
Stage stg;
// Stage 0 expressions (solo aplica a si == 1, pero permitimos en
// cualquier stage por simetria — el UI no las expone en stages 1+).
lua_getfield(L, -1, "expressions");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
std::string name = lua_tostring(L, -2);
std::string formula = lua_tostring(L, -1);
std::string cerr;
int id = lua_engine::compile(lua_engine::get(), formula, &cerr);
DerivedColumn d;
d.source_col = -1;
d.name = name;
d.formula = formula;
d.lua_id = id;
d.compile_error = (id < 0) ? cerr : "";
if (id >= 0 && si == 1) {
// auto-detect tipo via sample (solo para stage 0).
int sample = std::min(64, rows);
std::vector<std::string> samples_str;
std::vector<const char*> samples_ptr;
std::vector<std::string> hn_storage = headers;
std::unordered_map<std::string, int> n2c;
for (int c = 0; c < orig_cols && c < (int)hn_storage.size(); ++c) {
n2c[hn_storage[c]] = c;
}
for (int r = 0; r < sample; ++r) {
lua_engine::RowCtx ctx;
ctx.cells = cells;
ctx.orig_cols = orig_cols;
ctx.row = r;
ctx.header_names = &hn_storage;
ctx.name_to_col = &n2c;
std::string e;
samples_str.emplace_back(
lua_engine::eval(lua_engine::get(), id, ctx, &e));
}
for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
d.type = auto_detect_type(samples_ptr.data(),
(int)samples_ptr.size(), 1, 0);
} else {
d.type = ColumnType::String;
}
stg.derived.push_back(d);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// filter
lua_getfield(L, -1, "filter");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 1); std::string op = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col_name = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 3); std::string val = lua_to_string(L, -1); lua_pop(L, 1);
int ci = find_orig_col(cur_headers, col_name);
if (ci >= 0) {
stg.filters.push_back({ci, parse_op(op), val});
} else {
warn("stage " + std::to_string(si - 1) + ": filter col \"" + col_name + "\" not found");
}
if (op != "=" && op != "!=" && op != ">" && op != ">=" &&
op != "<" && op != "<=" && op != "contains" &&
op != "!contains" && op != "starts" && op != "ends") {
warn("stage " + std::to_string(si - 1) + ": unknown filter op \"" + op + "\" (defaulting to =)");
}
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// breakout (solo aplica stages >= 1, no-op silencioso si stage 0).
// Acepta sufijo ":granularity" para cols Date (fase 10).
lua_getfield(L, -1, "breakout");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_isstring(L, -1)) {
std::string bn = lua_tostring(L, -1);
std::string clean;
parse_breakout_granularity(bn, clean);
if (find_orig_col(cur_headers, clean) < 0) {
warn("stage " + std::to_string(si - 1) + ": breakout col \"" + clean + "\" not in input headers");
}
stg.breakouts.emplace_back(bn);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// aggregation
lua_getfield(L, -1, "aggregation");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 1) {
Aggregation a;
lua_rawgeti(L, -1, 1);
std::string fn_name = lua_to_string(L, -1);
lua_pop(L, 1);
bool known = (fn_name == "count" || fn_name == "sum" || fn_name == "avg" ||
fn_name == "min" || fn_name == "max" || fn_name == "distinct" ||
fn_name == "stddev"|| fn_name == "median" ||
fn_name == "p25" || fn_name == "p75" || fn_name == "p90" ||
fn_name == "p99" || fn_name == "percentile");
if (!known) {
warn("stage " + std::to_string(si - 1) + ": unknown aggregation fn \"" + fn_name + "\" (defaulting to count)");
}
a.fn = agg_fn_from_string(fn_name);
if (lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 2);
a.col = lua_to_string(L, -1);
lua_pop(L, 1);
if (a.fn != AggFn::Count && find_orig_col(cur_headers, a.col) < 0) {
warn("stage " + std::to_string(si - 1) + ": aggregation col \"" + a.col + "\" not in input headers");
}
} else if (a.fn != AggFn::Count) {
warn("stage " + std::to_string(si - 1) + ": aggregation \"" + fn_name + "\" requires a column");
}
if (lua_rawlen(L, -1) >= 3) {
lua_rawgeti(L, -1, 3);
if (lua_isnumber(L, -1)) a.arg = lua_tonumber(L, -1);
lua_pop(L, 1);
}
stg.aggregations.push_back(a);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// sort
lua_getfield(L, -1, "sort");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (lua_istable(L, -1) && lua_rawlen(L, -1) >= 2) {
lua_rawgeti(L, -1, 1); std::string dir = lua_to_string(L, -1); lua_pop(L, 1);
lua_rawgeti(L, -1, 2); std::string col = lua_to_string(L, -1); lua_pop(L, 1);
SortClause sc;
sc.col = col;
sc.desc = (dir == "desc");
if (dir != "asc" && dir != "desc") {
warn("stage " + std::to_string(si - 1) + ": unknown sort dir \"" + dir + "\" (defaulting to asc)");
}
stg.sorts.push_back(sc);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
state.stages.push_back(std::move(stg));
// Advance cur_headers para resolver filter/sort col del siguiente stage.
const Stage& last = state.stages.back();
if (si == 1) {
// Stage 0: cur_headers = orig + derived (sin breakouts/agg).
for (const auto& d : last.derived) cur_headers.push_back(d.name);
} else {
if (!last.breakouts.empty() || !last.aggregations.empty()) {
std::vector<std::string> next;
for (const auto& b : last.breakouts) next.push_back(b);
for (const auto& a : last.aggregations) next.push_back(aggregation_alias(a));
cur_headers = std::move(next);
}
}
lua_pop(L, 1); // pop stage entry
}
}
lua_pop(L, 1); // stages
state.ensure_stage0();
// ---- Walk columns (per-col render config) ----
int eff_cols = orig_cols + (int)state.raw().derived.size();
lua_getfield(L, -1, "columns");
if (lua_istable(L, -1)) {
state.col_visible.assign(eff_cols, true);
std::vector<std::pair<int,int>> order_pairs;
std::vector<bool> seen(eff_cols, false);
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
lua_getfield(L, -1, "name");
std::string nm = lua_to_string(L, -1);
lua_pop(L, 1);
int col_idx = find_orig_col(headers, nm);
if (col_idx < 0) {
int di = find_derived_idx(state.raw().derived, nm);
if (di >= 0) col_idx = orig_cols + di;
}
if (col_idx < 0 || col_idx >= eff_cols) { lua_pop(L, 1); continue; }
seen[col_idx] = true;
// visible
lua_getfield(L, -1, "visible");
if (lua_isboolean(L, -1)) state.col_visible[col_idx] = lua_toboolean(L, -1);
lua_pop(L, 1);
// order
lua_getfield(L, -1, "order");
int order_val = lua_isnumber(L, -1) ? (int)lua_tointeger(L, -1) : (col_idx + 1);
lua_pop(L, 1);
order_pairs.emplace_back(order_val, col_idx);
// type (mutable solo para derived)
lua_getfield(L, -1, "type");
if (lua_isstring(L, -1)) {
std::string tn = lua_tostring(L, -1);
ColumnType t = column_type_from_string(tn);
if (col_idx >= orig_cols) {
state.raw().derived[col_idx - orig_cols].type = t;
}
}
lua_pop(L, 1);
// color_rules
lua_getfield(L, -1, "color_rules");
if (lua_istable(L, -1)) {
int rn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= rn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "equals");
std::string eq = lua_to_string(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "color");
std::string hx = lua_to_string(L, -1);
lua_pop(L, 1);
state.color_rules.push_back({col_idx, eq, hex_to_color(hx)});
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_pop(L, 1); // pop entry
}
std::sort(order_pairs.begin(), order_pairs.end());
state.col_order.clear();
for (auto& p : order_pairs) state.col_order.push_back(p.second);
for (int c = 0; c < eff_cols; ++c) if (!seen[c]) state.col_order.push_back(c);
}
lua_pop(L, 1); // columns
// ---- Walk views[] (extra viz panels) ----
state.extra_panels.clear();
lua_getfield(L, -1, "views");
if (lua_istable(L, -1)) {
int n = (int)lua_rawlen(L, -1);
for (int i = 1; i <= n; ++i) {
lua_rawgeti(L, -1, i);
if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }
VizPanel p;
lua_getfield(L, -1, "display");
if (lua_isstring(L, -1)) p.display = view_mode_from_token(lua_tostring(L, -1));
lua_pop(L, 1);
auto read_str = [&](const char* key, std::string& out_s) {
lua_getfield(L, -1, key);
if (lua_isstring(L, -1)) out_s = lua_tostring(L, -1);
lua_pop(L, 1);
};
read_str("x_col", p.config.x_col);
read_str("cat_col", p.config.cat_col);
read_str("size_col", p.config.size_col);
lua_getfield(L, -1, "y_cols");
if (lua_istable(L, -1)) {
int yn = (int)lua_rawlen(L, -1);
for (int j = 1; j <= yn; ++j) {
lua_rawgeti(L, -1, j);
if (lua_isstring(L, -1)) p.config.y_cols.emplace_back(lua_tostring(L, -1));
lua_pop(L, 1);
}
}
lua_pop(L, 1);
lua_getfield(L, -1, "color");
if (lua_isstring(L, -1)) p.config.primary_color = hex_to_color(lua_tostring(L, -1));
lua_pop(L, 1);
lua_getfield(L, -1, "hist_bins");
if (lua_isnumber(L, -1)) p.config.hist_bins = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "pie_radius");
if (lua_isnumber(L, -1)) p.config.pie_radius = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_legend");
if (lua_isboolean(L, -1)) p.config.show_legend = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "show_markers");
if (lua_isboolean(L, -1)) p.config.show_markers = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, -1, "locked");
if (lua_isboolean(L, -1)) p.config.locked = lua_toboolean(L, -1);
lua_pop(L, 1);
// Panel 0 = main viz (state.display + state.viz_config).
if (i == 1) {
state.display = p.display;
state.viz_config = p.config;
} else {
state.extra_panels.push_back(p);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1); // views
lua_pop(L, 1); // pop root
return finish_with_warns(true);
}
} // namespace tql