100aeaa1fc
- 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>
914 lines
36 KiB
C++
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
|