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:
2026-05-13 00:50:35 +02:00
parent d782d463cb
commit 100aeaa1fc
12 changed files with 3040 additions and 291 deletions
+779
View File
@@ -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;
}