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>
This commit is contained in:
@@ -7,9 +7,12 @@
|
||||
// Exit 0 = todos los checks pasan, 1 = falla.
|
||||
|
||||
#include "data_table_logic.h"
|
||||
#include "llm_anthropic.h"
|
||||
#include "lua_engine.h"
|
||||
#include "tql.h"
|
||||
#include "tql_to_sql.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -2051,6 +2054,782 @@ return {
|
||||
check(join_strategy_from_token("nope") == JoinStrategy::Left, "phase9: parse fallback left");
|
||||
}
|
||||
|
||||
// === phase10: drill extendido ===
|
||||
{
|
||||
// truncate_date — granularities sobre 2026-05-12 (martes).
|
||||
std::string d = "2026-05-12";
|
||||
check(truncate_date(d, DateGranularity::Year) == "2026", "phase10: trunc year");
|
||||
check(truncate_date(d, DateGranularity::Month) == "2026-05", "phase10: trunc month");
|
||||
check(truncate_date(d, DateGranularity::Day) == "2026-05-12", "phase10: trunc day");
|
||||
check(truncate_date(d, DateGranularity::Week) == "2026-05-11", "phase10: trunc week (Mon)");
|
||||
check(truncate_date("2026-05-12T14:33:01", DateGranularity::Hour) == "2026-05-12T14",
|
||||
"phase10: trunc hour");
|
||||
check(truncate_date("not-a-date", DateGranularity::Month) == "not-a-date",
|
||||
"phase10: trunc passthrough invalido");
|
||||
check(truncate_date(d, DateGranularity::None) == d, "phase10: trunc None == identidad");
|
||||
}
|
||||
|
||||
{
|
||||
// auto_date_granularity
|
||||
check(auto_date_granularity("2024-01-01", "2026-05-12") == DateGranularity::Year,
|
||||
"phase10: auto year >2y");
|
||||
check(auto_date_granularity("2026-01-01", "2026-05-12") == DateGranularity::Month,
|
||||
"phase10: auto month >60d");
|
||||
check(auto_date_granularity("2026-04-15", "2026-05-12") == DateGranularity::Week,
|
||||
"phase10: auto week >14d");
|
||||
check(auto_date_granularity("2026-05-05", "2026-05-12") == DateGranularity::Day,
|
||||
"phase10: auto day <=14d");
|
||||
check(auto_date_granularity("bad", "2026-05-12") == DateGranularity::Day,
|
||||
"phase10: auto fallback day");
|
||||
}
|
||||
|
||||
{
|
||||
// parse_breakout_granularity
|
||||
std::string col;
|
||||
check(parse_breakout_granularity("ts:month", col) == DateGranularity::Month,
|
||||
"phase10: parse breakout month");
|
||||
check(col == "ts", "phase10: parse breakout col stripped");
|
||||
check(parse_breakout_granularity("ts", col) == DateGranularity::None,
|
||||
"phase10: parse breakout sin sufijo None");
|
||||
check(col == "ts", "phase10: col sin sufijo intacto");
|
||||
check(parse_breakout_granularity("ts:wat", col) == DateGranularity::None,
|
||||
"phase10: sufijo desconocido None");
|
||||
check(col == "ts:wat", "phase10: col preserva sufijo desconocido");
|
||||
}
|
||||
|
||||
{
|
||||
// compose_breakout
|
||||
check(compose_breakout("ts", DateGranularity::None) == "ts", "phase10: compose None");
|
||||
check(compose_breakout("ts", DateGranularity::Month) == "ts:month", "phase10: compose month");
|
||||
check(compose_breakout("ts", DateGranularity::Year) == "ts:year", "phase10: compose year");
|
||||
// round-trip parse(compose)
|
||||
std::string col;
|
||||
auto g = parse_breakout_granularity(compose_breakout("foo", DateGranularity::Week), col);
|
||||
check(g == DateGranularity::Week && col == "foo", "phase10: compose+parse round-trip");
|
||||
}
|
||||
|
||||
{
|
||||
// column_min_max
|
||||
const char* cells[] = {
|
||||
"2026-03-01",
|
||||
"2026-01-15",
|
||||
"",
|
||||
"2026-05-12",
|
||||
"2026-02-22",
|
||||
};
|
||||
std::string lo, hi;
|
||||
column_min_max(cells, 5, 1, 0, lo, hi);
|
||||
check(lo == "2026-01-15" && hi == "2026-05-12", "phase10: column_min_max ISO ordena lexical");
|
||||
|
||||
const char* empty_cells[] = {"", "", ""};
|
||||
column_min_max(empty_cells, 3, 1, 0, lo, hi);
|
||||
check(lo.empty() && hi.empty(), "phase10: column_min_max sin datos -> vacio");
|
||||
|
||||
column_min_max(cells, 5, 1, 5, lo, hi); // col fuera de rango
|
||||
check(lo.empty() && hi.empty(), "phase10: column_min_max col fuera de rango -> vacio");
|
||||
}
|
||||
|
||||
{
|
||||
// tokens round-trip granularity
|
||||
check(date_granularity_from_token("year") == DateGranularity::Year, "phase10: token year");
|
||||
check(date_granularity_from_token("month") == DateGranularity::Month, "phase10: token month");
|
||||
check(date_granularity_from_token("week") == DateGranularity::Week, "phase10: token week");
|
||||
check(date_granularity_from_token("day") == DateGranularity::Day, "phase10: token day");
|
||||
check(date_granularity_from_token("hour") == DateGranularity::Hour, "phase10: token hour");
|
||||
check(date_granularity_from_token("nope") == DateGranularity::None, "phase10: token fallback None");
|
||||
check(std::string(date_granularity_token(DateGranularity::Month)) == "month",
|
||||
"phase10: emit month");
|
||||
check(std::string(date_granularity_token(DateGranularity::None)) == "",
|
||||
"phase10: emit None empty");
|
||||
}
|
||||
|
||||
{
|
||||
// build_preset_filters
|
||||
auto f7 = build_preset_filters(FilterPreset::Last7d, 2, "2026-05-12");
|
||||
check(f7.size() == 1, "phase10: Last7d -> 1 filter");
|
||||
check(f7[0].col == 2 && f7[0].op == Op::Gte && f7[0].value == "2026-05-05",
|
||||
"phase10: Last7d -> Gte 2026-05-05");
|
||||
|
||||
auto f30 = build_preset_filters(FilterPreset::Last30d, 2, "2026-05-12");
|
||||
check(f30[0].value == "2026-04-12", "phase10: Last30d -> 2026-04-12");
|
||||
|
||||
auto f90 = build_preset_filters(FilterPreset::Last90d, 2, "2026-05-12");
|
||||
check(f90[0].value == "2026-02-11", "phase10: Last90d -> 2026-02-11");
|
||||
|
||||
auto fn0 = build_preset_filters(FilterPreset::ExcludeNulls, 3, "");
|
||||
check(fn0.size() == 1 && fn0[0].op == Op::Neq && fn0[0].value == "",
|
||||
"phase10: ExcludeNulls -> Neq ''");
|
||||
|
||||
auto fnz = build_preset_filters(FilterPreset::NonZero, 4, "");
|
||||
check(fnz.size() == 2, "phase10: NonZero -> 2 filters");
|
||||
check(fnz[0].op == Op::Neq && fnz[0].value == "" &&
|
||||
fnz[1].op == Op::Neq && fnz[1].value == "0",
|
||||
"phase10: NonZero -> Neq '' AND Neq '0'");
|
||||
|
||||
auto fbad = build_preset_filters(FilterPreset::Last7d, 2, "bad-date");
|
||||
check(fbad.empty(), "phase10: Last7d con today invalido -> empty");
|
||||
}
|
||||
|
||||
{
|
||||
// TQL round-trip: breakout con sufijo :granularity.
|
||||
State st0;
|
||||
st0.stages.resize(2);
|
||||
st0.stages[1].breakouts = {"ts:month"};
|
||||
Aggregation a; a.fn = AggFn::Count; a.alias = "n";
|
||||
st0.stages[1].aggregations.push_back(a);
|
||||
|
||||
std::vector<std::string> hdrs = {"ts", "amount"};
|
||||
std::vector<ColumnType> tys = {ColumnType::Date, ColumnType::Float};
|
||||
int eff = 2;
|
||||
std::string text = tql::emit(st0, hdrs, tys);
|
||||
check(text.find("\"ts:month\"") != std::string::npos,
|
||||
"phase10 TQL: emit breakout granularity sufijo");
|
||||
|
||||
std::string err;
|
||||
State st1;
|
||||
bool ok = tql::apply(text, st1, hdrs, tys, nullptr, 2, eff, &err);
|
||||
check(ok, "phase10 TQL: apply round-trip ok");
|
||||
check(st1.stages.size() >= 2 && st1.stages[1].breakouts.size() == 1 &&
|
||||
st1.stages[1].breakouts[0] == "ts:month",
|
||||
"phase10 TQL: breakout granularity preservada");
|
||||
}
|
||||
|
||||
{
|
||||
// compute_stage aplica truncado de fecha cuando hay :granularity.
|
||||
const char* cells[] = {
|
||||
"2026-01-15", "10",
|
||||
"2026-01-22", "20",
|
||||
"2026-02-03", "30",
|
||||
"2026-03-11", "40",
|
||||
};
|
||||
std::vector<std::string> hdrs = {"ts", "amount"};
|
||||
std::vector<ColumnType> tys = {ColumnType::Date, ColumnType::Float};
|
||||
Stage s1;
|
||||
s1.breakouts = {"ts:month"};
|
||||
Aggregation ag; ag.fn = AggFn::Count; ag.alias = "n";
|
||||
s1.aggregations.push_back(ag);
|
||||
auto out = compute_stage(cells, 4, 2, hdrs, tys, s1);
|
||||
check(out.rows == 3, "phase10: trunc month -> 3 grupos (Jan/Feb/Mar)");
|
||||
check(out.headers[0] == "ts:month", "phase10: header preserva sufijo");
|
||||
// Verifica que algun valor de breakout es "2026-01"
|
||||
bool found_jan = false;
|
||||
for (int r = 0; r < out.rows; ++r) {
|
||||
if (std::string(out.cells[r * out.cols + 0]) == "2026-01") found_jan = true;
|
||||
}
|
||||
check(found_jan, "phase10: trunc value '2026-01' presente");
|
||||
}
|
||||
|
||||
// === phase10 hit-tests para click-to-drill ===
|
||||
{
|
||||
// nearest_index_1d
|
||||
double xs[] = {0, 1, 2, 3, 4};
|
||||
check(nearest_index_1d(0.0, xs, 5) == 0, "phase10 hit: nearest_1d exact 0");
|
||||
check(nearest_index_1d(2.4, xs, 5) == 2, "phase10 hit: nearest_1d 2.4 -> 2");
|
||||
check(nearest_index_1d(2.6, xs, 5) == 3, "phase10 hit: nearest_1d 2.6 -> 3");
|
||||
check(nearest_index_1d(-1.0, xs, 5) == 0, "phase10 hit: nearest_1d clamp left");
|
||||
check(nearest_index_1d(99.0, xs, 5) == 4, "phase10 hit: nearest_1d clamp right");
|
||||
check(nearest_index_1d(0.0, nullptr, 0) == -1, "phase10 hit: nearest_1d empty -> -1");
|
||||
}
|
||||
|
||||
{
|
||||
// nearest_index_2d
|
||||
double xs[] = {0, 10, 5, 5};
|
||||
double ys[] = {0, 0, 10, 5};
|
||||
check(nearest_index_2d(0.1, 0.1, xs, ys, 4) == 0, "phase10 hit: nearest_2d cerca de (0,0)");
|
||||
check(nearest_index_2d(9.9, 0.0, xs, ys, 4) == 1, "phase10 hit: nearest_2d cerca de (10,0)");
|
||||
check(nearest_index_2d(5.0, 4.9, xs, ys, 4) == 3, "phase10 hit: nearest_2d cerca de (5,5)");
|
||||
check(nearest_index_2d(0, 0, nullptr, nullptr, 0) == -1, "phase10 hit: nearest_2d empty -> -1");
|
||||
}
|
||||
|
||||
{
|
||||
// pie_angle (convencion ImPlot: 0 = top, sentido horario)
|
||||
const double PI = 3.14159265358979323846;
|
||||
double a;
|
||||
a = pie_angle(0.5, 0.5, 0.5, 0.0); // top
|
||||
check(std::fabs(a - 0.0) < 1e-9, "phase10 hit: pie_angle top = 0");
|
||||
a = pie_angle(0.5, 0.5, 1.0, 0.5); // right -> PI/2
|
||||
check(std::fabs(a - PI/2) < 1e-9, "phase10 hit: pie_angle right = PI/2");
|
||||
a = pie_angle(0.5, 0.5, 0.5, 1.0); // bottom -> PI
|
||||
check(std::fabs(a - PI) < 1e-9, "phase10 hit: pie_angle bottom = PI");
|
||||
a = pie_angle(0.5, 0.5, 0.0, 0.5); // left -> 3*PI/2
|
||||
check(std::fabs(a - 3*PI/2) < 1e-9, "phase10 hit: pie_angle left = 3PI/2");
|
||||
}
|
||||
|
||||
{
|
||||
// pie_slice_at_angle: 4 slices iguales -> cada uno cubre PI/2.
|
||||
double sums[] = {1.0, 1.0, 1.0, 1.0};
|
||||
const double PI = 3.14159265358979323846;
|
||||
check(pie_slice_at_angle(0.0, sums, 4) == 0, "phase10 hit: slice 0 (top)");
|
||||
check(pie_slice_at_angle(PI/4, sums, 4) == 0, "phase10 hit: slice 0 (mid)");
|
||||
check(pie_slice_at_angle(PI/2 + 0.1, sums, 4) == 1, "phase10 hit: slice 1");
|
||||
check(pie_slice_at_angle(PI + 0.1, sums, 4) == 2, "phase10 hit: slice 2");
|
||||
check(pie_slice_at_angle(3*PI/2 + 0.1, sums, 4) == 3, "phase10 hit: slice 3");
|
||||
|
||||
double zeros[] = {0.0, 0.0};
|
||||
check(pie_slice_at_angle(0.5, zeros, 2) == -1, "phase10 hit: total 0 -> -1");
|
||||
check(pie_slice_at_angle(0.0, nullptr, 0) == -1, "phase10 hit: empty -> -1");
|
||||
|
||||
double neg[] = {1.0, -1.0};
|
||||
check(pie_slice_at_angle(0.5, neg, 2) == -1, "phase10 hit: neg sum -> -1");
|
||||
}
|
||||
|
||||
{
|
||||
// heatmap_cell_at
|
||||
int rr, cc;
|
||||
heatmap_cell_at(1.5, 2.5, 4, 3, rr, cc);
|
||||
check(rr == 2 && cc == 1, "phase10 hit: heatmap (1.5,2.5) en 4x3 -> r2 c1");
|
||||
heatmap_cell_at(-1, 0, 4, 3, rr, cc);
|
||||
check(rr == -1 && cc == -1, "phase10 hit: heatmap fuera de rango");
|
||||
heatmap_cell_at(0, 0, 0, 0, rr, cc);
|
||||
check(rr == -1 && cc == -1, "phase10 hit: heatmap empty");
|
||||
}
|
||||
|
||||
{
|
||||
// E2E click-to-drill: simular pipeline stage1 agrupado, click en row idx 2.
|
||||
State st;
|
||||
st.stages.resize(2);
|
||||
std::vector<std::string> hdrs = {"lang", "n"};
|
||||
std::vector<ColumnType> tys = {ColumnType::String, ColumnType::Int};
|
||||
st.stages[1].breakouts.push_back("lang");
|
||||
st.stages[1].aggregations.push_back({AggFn::Count});
|
||||
st.active_stage = 1;
|
||||
|
||||
// Stage 1 output simulado (3 grupos).
|
||||
const char* g_cells[] = {
|
||||
"go", "3",
|
||||
"py", "2",
|
||||
"cpp", "1",
|
||||
};
|
||||
StageOutput so;
|
||||
so.cells.insert(so.cells.end(), g_cells, g_cells + 6);
|
||||
so.rows = 3;
|
||||
so.cols = 2;
|
||||
so.headers = {"lang", "count"};
|
||||
|
||||
// Simular click en row idx 2 (cpp).
|
||||
int clicked_row = 2;
|
||||
int n_brk = (int)st.stages[1].breakouts.size();
|
||||
check(n_brk == 1, "phase10 e2e: 1 breakout");
|
||||
const char* v = so.cells[clicked_row * so.cols + 0];
|
||||
std::string col_clean;
|
||||
parse_breakout_granularity(so.headers[0], col_clean);
|
||||
check(col_clean == "lang", "phase10 e2e: col_clean stripped OK");
|
||||
st.stages[0].filters.push_back(make_drill_filter(0, v));
|
||||
st.active_stage = 0;
|
||||
|
||||
check(st.active_stage == 0, "phase10 e2e: active retrocede a 0");
|
||||
check(st.stages[0].filters.size() == 1, "phase10 e2e: 1 filter anadido");
|
||||
check(st.stages[0].filters[0].col == 0 &&
|
||||
st.stages[0].filters[0].op == Op::Eq &&
|
||||
st.stages[0].filters[0].value == "cpp",
|
||||
"phase10 e2e: filter Op::Eq col=0 value=cpp");
|
||||
}
|
||||
|
||||
// === phase10 drill history (apply/undo step) ===
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(2);
|
||||
st.active_stage = 1;
|
||||
|
||||
DrillStep step;
|
||||
step.target_stage = 0;
|
||||
step.filter_pos = 0;
|
||||
step.prev_active_stage = 1;
|
||||
step.added = make_drill_filter(0, "go");
|
||||
|
||||
check(apply_drill_step(st, step), "phase10 hist: apply ok");
|
||||
check(st.stages[0].filters.size() == 1, "phase10 hist: filter anadido");
|
||||
check(st.stages[0].filters[0].value == "go", "phase10 hist: value preservado");
|
||||
check(st.active_stage == 0, "phase10 hist: active = target");
|
||||
|
||||
check(undo_drill_step(st, step), "phase10 hist: undo ok");
|
||||
check(st.stages[0].filters.empty(), "phase10 hist: filter eliminado");
|
||||
check(st.active_stage == 1, "phase10 hist: active restaurado");
|
||||
|
||||
// Redo
|
||||
check(apply_drill_step(st, step), "phase10 hist: redo ok");
|
||||
check(st.stages[0].filters.size() == 1, "phase10 hist: redo filter de vuelta");
|
||||
check(st.active_stage == 0, "phase10 hist: redo active retorna");
|
||||
|
||||
// Edge: target fuera de rango
|
||||
DrillStep bad;
|
||||
bad.target_stage = 99;
|
||||
check(!apply_drill_step(st, bad), "phase10 hist: apply fuera de rango -> false");
|
||||
check(!undo_drill_step(st, bad), "phase10 hist: undo fuera de rango -> false");
|
||||
|
||||
// Edge: pos invalida
|
||||
DrillStep bad_pos = step;
|
||||
bad_pos.filter_pos = 99;
|
||||
check(!undo_drill_step(st, bad_pos), "phase10 hist: undo pos invalida -> false");
|
||||
}
|
||||
|
||||
// === phase10 drill history: back/forward stack semantics simulado ===
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(3);
|
||||
st.active_stage = 2;
|
||||
|
||||
std::vector<DrillStep> back_stack;
|
||||
std::vector<DrillStep> fwd_stack;
|
||||
|
||||
auto drill = [&](int from, int target, int pos, int col, const std::string& v) {
|
||||
DrillStep s;
|
||||
s.target_stage = target;
|
||||
s.filter_pos = pos;
|
||||
s.prev_active_stage = from;
|
||||
s.added = make_drill_filter(col, v);
|
||||
apply_drill_step(st, s);
|
||||
back_stack.push_back(s);
|
||||
fwd_stack.clear();
|
||||
};
|
||||
|
||||
drill(2, 1, 0, 0, "go");
|
||||
check(st.stages[1].filters.size() == 1, "phase10 hist seq: drill1 aplicado");
|
||||
drill(1, 0, 0, 1, "10");
|
||||
check(st.stages[0].filters.size() == 1, "phase10 hist seq: drill2 aplicado");
|
||||
check(back_stack.size() == 2, "phase10 hist seq: back stack 2");
|
||||
check(fwd_stack.empty(), "phase10 hist seq: forward limpio");
|
||||
|
||||
// Back x1
|
||||
DrillStep s = back_stack.back(); back_stack.pop_back();
|
||||
undo_drill_step(st, s);
|
||||
fwd_stack.push_back(s);
|
||||
check(st.stages[0].filters.empty(), "phase10 hist seq: back deshace drill2");
|
||||
check(st.active_stage == 1, "phase10 hist seq: back restaura active=1");
|
||||
check(fwd_stack.size() == 1, "phase10 hist seq: fwd stack 1");
|
||||
|
||||
// Forward x1
|
||||
s = fwd_stack.back(); fwd_stack.pop_back();
|
||||
apply_drill_step(st, s);
|
||||
back_stack.push_back(s);
|
||||
check(st.stages[0].filters.size() == 1, "phase10 hist seq: forward reaplica");
|
||||
check(st.active_stage == 0, "phase10 hist seq: forward active=0");
|
||||
}
|
||||
|
||||
// === phase10 row inspector (row_to_tsv + build_filters_from_row) ===
|
||||
{
|
||||
const char* cells[] = {
|
||||
"go", "10", "filter",
|
||||
"py", "20", "sma",
|
||||
"go", "30", "map",
|
||||
};
|
||||
std::vector<std::string> hdrs = {"lang", "n", "fn"};
|
||||
|
||||
std::string tsv = row_to_tsv(cells, 3, 3, 1, hdrs);
|
||||
check(tsv == "lang\tn\tfn\r\npy\t20\tsma\r\n",
|
||||
"phase10 inspect: row_to_tsv layout");
|
||||
|
||||
check(row_to_tsv(cells, 3, 3, -1, hdrs).empty(), "phase10 inspect: tsv neg row -> empty");
|
||||
check(row_to_tsv(cells, 3, 3, 5, hdrs).empty(), "phase10 inspect: tsv row oob -> empty");
|
||||
check(row_to_tsv(cells, 3, 0, 0, hdrs).empty(), "phase10 inspect: tsv cols=0 -> empty");
|
||||
|
||||
auto fs = build_filters_from_row(cells, 3, 3, 0);
|
||||
check(fs.size() == 3, "phase10 inspect: 3 filters de row 0");
|
||||
check(fs[0].col == 0 && fs[0].op == Op::Eq && fs[0].value == "go",
|
||||
"phase10 inspect: filter[0] col=0 op=Eq value=go");
|
||||
check(fs[2].value == "filter", "phase10 inspect: filter[2] value=filter");
|
||||
|
||||
// Row con celda vacia -> filter saltado
|
||||
const char* sparse[] = {"a", "", "c"};
|
||||
auto fs2 = build_filters_from_row(sparse, 1, 3, 0);
|
||||
check(fs2.size() == 2 && fs2[0].col == 0 && fs2[1].col == 2,
|
||||
"phase10 inspect: cells vacios salteados");
|
||||
|
||||
check(build_filters_from_row(cells, 3, 3, -1).empty(),
|
||||
"phase10 inspect: build_filters row invalido -> empty");
|
||||
}
|
||||
|
||||
// === phase10 drill-up ===
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(3);
|
||||
st.active_stage = 2;
|
||||
check(drill_up(st), "phase10 up: 2->1 ok");
|
||||
check(st.active_stage == 1, "phase10 up: active=1");
|
||||
check(drill_up(st), "phase10 up: 1->0 ok");
|
||||
check(st.active_stage == 0, "phase10 up: active=0");
|
||||
check(!drill_up(st), "phase10 up: 0 -> false");
|
||||
check(st.active_stage == 0, "phase10 up: queda en 0");
|
||||
|
||||
// Filters no se mueven
|
||||
State st2;
|
||||
st2.stages.resize(2);
|
||||
st2.active_stage = 1;
|
||||
st2.stages[1].filters.push_back({0, Op::Eq, "x"});
|
||||
drill_up(st2);
|
||||
check(st2.stages[0].filters.empty() && st2.stages[1].filters.size() == 1,
|
||||
"phase10 up: filters quedan en su stage");
|
||||
|
||||
State empty_st;
|
||||
check(!drill_up(empty_st), "phase10 up: stages vacio -> false");
|
||||
}
|
||||
|
||||
// === phase11: Lua subset validator + transpiler ===
|
||||
{
|
||||
std::string err;
|
||||
// Subset OK: literales + ops
|
||||
std::string e1 = tql_to_sql::transpile_expr("1 + 2", {}, err);
|
||||
check(err.empty() && e1.find("1 + 2") != std::string::npos,
|
||||
"phase11 lua: literal arith");
|
||||
|
||||
std::string e2 = tql_to_sql::transpile_expr("[a] + [b] * 2", {}, err);
|
||||
check(err.empty() && e2.find("\"a\"") != std::string::npos &&
|
||||
e2.find("\"b\"") != std::string::npos,
|
||||
"phase11 lua: col refs + arith");
|
||||
|
||||
std::string e3 = tql_to_sql::transpile_expr("[a] .. \"_\" .. [b]", {}, err);
|
||||
check(err.empty() && e3.find(" || ") != std::string::npos,
|
||||
"phase11 lua: concat -> ||");
|
||||
|
||||
std::string e4 = tql_to_sql::transpile_expr(
|
||||
"if [n] > 10 then \"big\" else \"small\" end", {}, err);
|
||||
check(err.empty() && e4.find("CASE WHEN") != std::string::npos &&
|
||||
e4.find("THEN") != std::string::npos && e4.find("ELSE") != std::string::npos,
|
||||
"phase11 lua: if/then/else -> CASE");
|
||||
|
||||
std::string e5 = tql_to_sql::transpile_expr("math.floor([x] / 100)", {}, err);
|
||||
check(err.empty() && e5.find("floor(") != std::string::npos,
|
||||
"phase11 lua: math.floor");
|
||||
|
||||
std::string e6 = tql_to_sql::transpile_expr("string.upper([name])", {}, err);
|
||||
check(err.empty() && e6.find("upper(") != std::string::npos,
|
||||
"phase11 lua: string.upper");
|
||||
|
||||
std::string e7 = tql_to_sql::transpile_expr("string.sub([s], 1, 3)", {}, err);
|
||||
check(err.empty() && e7.find("substring(") != std::string::npos,
|
||||
"phase11 lua: string.sub 3-arg");
|
||||
|
||||
std::string e8 = tql_to_sql::transpile_expr("not ([x] == nil)", {}, err);
|
||||
check(err.empty() && e8.find("NOT") != std::string::npos && e8.find("NULL") != std::string::npos,
|
||||
"phase11 lua: not + nil");
|
||||
|
||||
std::string e9 = tql_to_sql::transpile_expr("tonumber([n])", {}, err);
|
||||
check(err.empty() && e9.find("CAST(") != std::string::npos,
|
||||
"phase11 lua: tonumber -> CAST DOUBLE");
|
||||
|
||||
// Fuera subset: 9 categorias rechazadas
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("function() return 1 end", {}, err).empty()
|
||||
&& err.find("closures") != std::string::npos,
|
||||
"phase11 lua: function closure rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("local x = 1", {}, err).empty()
|
||||
&& err.find("local") != std::string::npos,
|
||||
"phase11 lua: local rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("for i=1,10 do end", {}, err).empty()
|
||||
&& err.find("loops") != std::string::npos,
|
||||
"phase11 lua: for loop rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("while true do end", {}, err).empty()
|
||||
&& err.find("loops") != std::string::npos,
|
||||
"phase11 lua: while loop rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("{1,2,3}", {}, err).empty()
|
||||
&& err.find("table") != std::string::npos,
|
||||
"phase11 lua: table literal rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("io.read()", {}, err).empty()
|
||||
&& err.find("io") != std::string::npos,
|
||||
"phase11 lua: io.* rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("string.gsub([s], \"a\", \"b\")", {}, err).empty()
|
||||
&& err.find("whitelist") != std::string::npos,
|
||||
"phase11 lua: string.gsub no whitelisted");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("print([x])", {}, err).empty()
|
||||
&& err.find("print") != std::string::npos,
|
||||
"phase11 lua: print rechazado");
|
||||
|
||||
err.clear();
|
||||
check(tql_to_sql::transpile_expr("[a]; [b]", {}, err).empty()
|
||||
&& err.find("multi-statement") != std::string::npos,
|
||||
"phase11 lua: ';' multi-stmt rechazado");
|
||||
|
||||
// is_transpilable wrapper
|
||||
std::string werr;
|
||||
check(tql_to_sql::is_transpilable("[a] + 1", werr), "phase11 lua: is_transpilable OK");
|
||||
check(!tql_to_sql::is_transpilable("function() end", werr),
|
||||
"phase11 lua: is_transpilable false para closure");
|
||||
}
|
||||
|
||||
// === phase11: TQL State -> SQL DuckDB emit ===
|
||||
{
|
||||
// Setup: 1 tabla "users" con cols lang,n.
|
||||
TableInput t;
|
||||
t.name = "users";
|
||||
t.headers = {"lang", "n"};
|
||||
t.types = {ColumnType::String, ColumnType::Int};
|
||||
// Cells no usado por emit (solo schema).
|
||||
std::vector<TableInput> tables = {t};
|
||||
|
||||
// Caso 1: stage 0 simple (sin filters ni sort)
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: empty pipeline -> no error");
|
||||
check(e.sql.find("WITH t0") != std::string::npos &&
|
||||
e.sql.find("FROM \"users\"") != std::string::npos &&
|
||||
e.sql.find("SELECT * FROM t0") != std::string::npos,
|
||||
"phase11 sql: stage0 SELECT * FROM users");
|
||||
}
|
||||
|
||||
// Caso 2: stage 0 filter + sort
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
st.stages[0].filters.push_back({0, Op::Eq, "go"});
|
||||
st.stages[0].filters.push_back({1, Op::Gt, "10"});
|
||||
st.stages[0].sorts.push_back({"n", true});
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: filter+sort OK");
|
||||
check(e.sql.find("WHERE") != std::string::npos &&
|
||||
e.sql.find("\"lang\" = ?") != std::string::npos &&
|
||||
e.sql.find("\"n\" > ?") != std::string::npos,
|
||||
"phase11 sql: filter clauses");
|
||||
check(e.params.size() == 2 && e.params[0] == "go" && e.params[1] == "10",
|
||||
"phase11 sql: params bound");
|
||||
check(e.sql.find("ORDER BY \"n\" DESC") != std::string::npos,
|
||||
"phase11 sql: ORDER BY desc");
|
||||
}
|
||||
|
||||
// Caso 3: stage 1 group + count
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(2);
|
||||
st.stages[1].breakouts.push_back("lang");
|
||||
st.stages[1].aggregations.push_back({AggFn::Count});
|
||||
st.active_stage = 1;
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: group ok");
|
||||
check(e.sql.find("t1 AS") != std::string::npos &&
|
||||
e.sql.find("COUNT(*)") != std::string::npos &&
|
||||
e.sql.find("GROUP BY") != std::string::npos &&
|
||||
e.sql.find("SELECT * FROM t1") != std::string::npos,
|
||||
"phase11 sql: stage1 CTE + COUNT + GROUP BY");
|
||||
}
|
||||
|
||||
// Caso 4: granularity :month -> date_trunc
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(2);
|
||||
st.stages[1].breakouts.push_back("ts:month");
|
||||
st.stages[1].aggregations.push_back({AggFn::Sum, "n"});
|
||||
st.active_stage = 1;
|
||||
TableInput ts_t;
|
||||
ts_t.name = "events";
|
||||
ts_t.headers = {"ts", "n"};
|
||||
ts_t.types = {ColumnType::Date, ColumnType::Int};
|
||||
std::vector<TableInput> tt = {ts_t};
|
||||
auto e = tql_to_sql::emit_sql(st, tt);
|
||||
check(e.error.empty(), "phase11 sql: granularity ok");
|
||||
check(e.sql.find("date_trunc('month'") != std::string::npos &&
|
||||
e.sql.find("SUM(\"n\")") != std::string::npos,
|
||||
"phase11 sql: date_trunc + SUM");
|
||||
}
|
||||
|
||||
// Caso 5: aggregations p25/median/p99
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(2);
|
||||
st.stages[1].breakouts.push_back("lang");
|
||||
st.stages[1].aggregations.push_back({AggFn::Median, "n"});
|
||||
st.stages[1].aggregations.push_back({AggFn::P25, "n"});
|
||||
st.stages[1].aggregations.push_back({AggFn::P99, "n"});
|
||||
st.active_stage = 1;
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: percentiles ok");
|
||||
check(e.sql.find("quantile_cont(\"n\", 0.5)") != std::string::npos &&
|
||||
e.sql.find("quantile_cont(\"n\", 0.25)") != std::string::npos &&
|
||||
e.sql.find("quantile_cont(\"n\", 0.99)") != std::string::npos,
|
||||
"phase11 sql: quantile_cont calls");
|
||||
}
|
||||
|
||||
// Caso 6: joins 4 strategies
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
Join jn;
|
||||
jn.alias = "o";
|
||||
jn.source = "orders";
|
||||
jn.on.push_back({"user_id", "user_id"});
|
||||
jn.strategy = JoinStrategy::Left;
|
||||
st.joins.push_back(jn);
|
||||
TableInput u, o;
|
||||
u.name = "users";
|
||||
u.headers = {"user_id", "name"};
|
||||
u.types = {ColumnType::String, ColumnType::String};
|
||||
o.name = "orders";
|
||||
o.headers = {"user_id", "amount"};
|
||||
o.types = {ColumnType::String, ColumnType::Int};
|
||||
std::vector<TableInput> tt = {u, o};
|
||||
auto e = tql_to_sql::emit_sql(st, tt);
|
||||
check(e.error.empty(), "phase11 sql: join ok");
|
||||
check(e.sql.find("LEFT JOIN \"orders\" AS \"o\"") != std::string::npos &&
|
||||
e.sql.find("ON \"users\".\"user_id\" = \"o\".\"user_id\"") != std::string::npos,
|
||||
"phase11 sql: LEFT JOIN ON syntax");
|
||||
|
||||
// Inner
|
||||
st.joins[0].strategy = JoinStrategy::Inner;
|
||||
auto e2 = tql_to_sql::emit_sql(st, tt);
|
||||
check(e2.sql.find("INNER JOIN") != std::string::npos, "phase11 sql: INNER JOIN");
|
||||
|
||||
// Right
|
||||
st.joins[0].strategy = JoinStrategy::Right;
|
||||
auto e3 = tql_to_sql::emit_sql(st, tt);
|
||||
check(e3.sql.find("RIGHT JOIN") != std::string::npos, "phase11 sql: RIGHT JOIN");
|
||||
|
||||
// Full
|
||||
st.joins[0].strategy = JoinStrategy::Full;
|
||||
auto e4 = tql_to_sql::emit_sql(st, tt);
|
||||
check(e4.sql.find("FULL OUTER JOIN") != std::string::npos, "phase11 sql: FULL OUTER JOIN");
|
||||
}
|
||||
|
||||
// Caso 7: derived col subset -> SQL expression
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
DerivedColumn d;
|
||||
d.name = "size_kb";
|
||||
d.source_col = -1;
|
||||
d.formula = "[n] / 1024.0";
|
||||
d.type = ColumnType::Float;
|
||||
st.stages[0].derived.push_back(d);
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: derived subset ok");
|
||||
check(e.sql.find("\"n\" / 1024") != std::string::npos &&
|
||||
e.sql.find("AS \"size_kb\"") != std::string::npos,
|
||||
"phase11 sql: derived expression + alias");
|
||||
}
|
||||
|
||||
// Caso 8: derived col FUERA subset -> warning + skip
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
DerivedColumn d;
|
||||
d.name = "bad";
|
||||
d.source_col = -1;
|
||||
d.formula = "string.gsub([n], \"a\", \"b\")";
|
||||
d.type = ColumnType::String;
|
||||
st.stages[0].derived.push_back(d);
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: derived fuera subset NO bloquea emit");
|
||||
check(!e.warnings.empty() &&
|
||||
e.warnings[0].find("out of SQL subset") != std::string::npos,
|
||||
"phase11 sql: warning derived fuera subset");
|
||||
check(e.sql.find("\"bad\"") == std::string::npos,
|
||||
"phase11 sql: derived skip cuando fuera subset");
|
||||
}
|
||||
|
||||
// Caso 9: empty tables -> error
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
std::vector<TableInput> empty;
|
||||
auto e = tql_to_sql::emit_sql(st, empty);
|
||||
check(!e.error.empty() && e.error.find("no input tables") != std::string::npos,
|
||||
"phase11 sql: empty tables -> error");
|
||||
}
|
||||
|
||||
// Caso 10: stage 0 con LIKE (Contains)
|
||||
{
|
||||
State st;
|
||||
st.stages.resize(1);
|
||||
st.stages[0].filters.push_back({0, Op::Contains, "go"});
|
||||
auto e = tql_to_sql::emit_sql(st, tables);
|
||||
check(e.error.empty(), "phase11 sql: LIKE Contains ok");
|
||||
check(e.sql.find("LIKE ?") != std::string::npos &&
|
||||
e.params.size() == 1 && e.params[0] == "%go%",
|
||||
"phase11 sql: Contains -> LIKE %go%");
|
||||
}
|
||||
}
|
||||
|
||||
// === phase11: LLM client (mock, no red) ===
|
||||
{
|
||||
llm_anthropic::AskInput in;
|
||||
in.question = "show top 10 langs";
|
||||
in.tql_current = "return { stages = {} }";
|
||||
in.col_names = {"lang", "n"};
|
||||
in.col_types = {ColumnType::String, ColumnType::Int};
|
||||
in.mode = llm_anthropic::OutputMode::TQL;
|
||||
std::string body = llm_anthropic::build_request_body(in);
|
||||
check(body.find("\"model\":\"claude-sonnet-4-6\"") != std::string::npos,
|
||||
"phase11 llm: default model");
|
||||
check(body.find("\"max_tokens\":8192") != std::string::npos,
|
||||
"phase11 llm: max_tokens");
|
||||
check(body.find("\\\"system\\\"") == std::string::npos /* not double-escaped */,
|
||||
"phase11 llm: system not double-escaped");
|
||||
check(body.find("Available columns") != std::string::npos,
|
||||
"phase11 llm: schema block present");
|
||||
check(body.find("show top 10 langs") != std::string::npos,
|
||||
"phase11 llm: question present");
|
||||
check(body.find("TQL") != std::string::npos,
|
||||
"phase11 llm: system mentions TQL");
|
||||
|
||||
in.mode = llm_anthropic::OutputMode::SQL;
|
||||
std::string body_sql = llm_anthropic::build_request_body(in);
|
||||
check(body_sql.find("DuckDB") != std::string::npos,
|
||||
"phase11 llm: SQL mode mentions DuckDB");
|
||||
}
|
||||
|
||||
{
|
||||
// extract_code_block
|
||||
std::string raw1 = "Here you go:\n```lua\nreturn { x = 1 }\n```\nDone!";
|
||||
std::string code = llm_anthropic::extract_code_block(raw1, "lua");
|
||||
check(code == "return { x = 1 }", "phase11 llm: extract ```lua block");
|
||||
|
||||
std::string raw2 = "Sure:\n```\nplain code\n```";
|
||||
std::string code2 = llm_anthropic::extract_code_block(raw2, "lua");
|
||||
check(code2 == "plain code", "phase11 llm: extract bare ```");
|
||||
|
||||
std::string raw3 = "no fences here";
|
||||
std::string code3 = llm_anthropic::extract_code_block(raw3, "lua");
|
||||
check(code3 == "no fences here", "phase11 llm: no fence -> stripped");
|
||||
|
||||
std::string raw4 = "```sql\nSELECT 1;\n```";
|
||||
std::string code4 = llm_anthropic::extract_code_block(raw4, "sql");
|
||||
check(code4 == "SELECT 1;", "phase11 llm: extract ```sql");
|
||||
}
|
||||
|
||||
{
|
||||
// parse_response_text from JSON
|
||||
std::string j = "{\"id\":\"x\",\"content\":[{\"type\":\"text\",\"text\":\"hello\\nworld\"}],\"role\":\"assistant\"}";
|
||||
std::string t = llm_anthropic::parse_response_text(j);
|
||||
check(t == "hello\nworld", "phase11 llm: parse text content");
|
||||
|
||||
std::string j2 = "{\"content\":[{\"type\":\"text\",\"text\":\"\\\"quoted\\\"\"}]}";
|
||||
std::string t2 = llm_anthropic::parse_response_text(j2);
|
||||
check(t2 == "\"quoted\"", "phase11 llm: parse quoted escape");
|
||||
|
||||
std::string j3 = "{\"error\":\"foo\"}";
|
||||
std::string t3 = llm_anthropic::parse_response_text(j3);
|
||||
check(t3.empty(), "phase11 llm: no text -> empty");
|
||||
}
|
||||
|
||||
{
|
||||
// Mock end-to-end via FN_LLM_MOCK_RESPONSE (portable Linux/Mingw via putenv).
|
||||
const char* mock_kv =
|
||||
"FN_LLM_MOCK_RESPONSE={\"content\":[{\"type\":\"text\",\"text\":\"```lua\\nreturn { mock = true }\\n```\"}]}";
|
||||
putenv((char*)mock_kv);
|
||||
llm_anthropic::AskInput in;
|
||||
in.question = "q";
|
||||
in.col_names = {"a"};
|
||||
in.col_types = {ColumnType::String};
|
||||
auto r = llm_anthropic::ask(in);
|
||||
check(r.error.empty(), "phase11 llm mock: no error");
|
||||
check(r.code == "return { mock = true }", "phase11 llm mock: code extracted");
|
||||
// Unset: putenv con "VAR=" deja vacio (suficiente para nuestro check `*mock`).
|
||||
putenv((char*)"FN_LLM_MOCK_RESPONSE=");
|
||||
}
|
||||
|
||||
std::printf("\n=== %d passed, %d failed ===\n", passed, failed);
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user