Files
fn_registry/cpp/tests/test_tql_to_sql.cpp
T
egutierrez 212875ed0d chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

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

260 lines
8.5 KiB
C++

// test_tql_to_sql.cpp — Catch2 tests for tql_to_sql (pure SQL emitter).
// Issue 0081. No DuckDB linked — only validates emitted SQL strings.
#include "catch_amalgamated.hpp"
#include "core/tql_to_sql.h"
#include "core/tql_helpers.h"
#include <string>
#include <vector>
using namespace data_table;
using namespace tql_to_sql;
// Helper: build a minimal State with one Stage.
static State make_state_one_stage() {
State st;
st.stages.push_back(Stage{});
st.active_stage = 0;
return st;
}
// Helper: build a TableInput for tests.
static TableInput make_table(const std::string& name,
const std::vector<std::string>& headers) {
TableInput ti;
ti.name = name;
ti.headers = headers;
return ti;
}
// ============================================================================
// transpile_expr tests
// ============================================================================
TEST_CASE("transpile_expr: simple arithmetic", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {"price", "qty"};
std::string out = transpile_expr("[price] * [qty]", h, err);
REQUIRE(err.empty());
REQUIRE(out.find("\"price\"") != std::string::npos);
REQUIRE(out.find("\"qty\"") != std::string::npos);
REQUIRE(out.find(" * ") != std::string::npos);
}
TEST_CASE("transpile_expr: numeric literal", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {"x"};
std::string out = transpile_expr("42", h, err);
REQUIRE(err.empty());
REQUIRE(out == "42");
}
TEST_CASE("transpile_expr: string literal", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {};
std::string out = transpile_expr("\"hello\"", h, err);
REQUIRE(err.empty());
REQUIRE(out == "'hello'");
}
TEST_CASE("transpile_expr: boolean literals", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h;
REQUIRE(transpile_expr("true", h, err) == "TRUE");
REQUIRE(err.empty());
REQUIRE(transpile_expr("false", h, err) == "FALSE");
REQUIRE(err.empty());
REQUIRE(transpile_expr("nil", h, err) == "NULL");
REQUIRE(err.empty());
}
TEST_CASE("transpile_expr: ternary if/then/else", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {"score"};
std::string out = transpile_expr("if [score] > 90 then \"A\" else \"B\" end", h, err);
REQUIRE(err.empty());
REQUIRE(out.find("CASE WHEN") != std::string::npos);
REQUIRE(out.find("THEN") != std::string::npos);
REQUIRE(out.find("ELSE") != std::string::npos);
REQUIRE(out.find("END") != std::string::npos);
}
TEST_CASE("transpile_expr: string concat", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {"first", "last"};
std::string out = transpile_expr("[first] .. \" \" .. [last]", h, err);
REQUIRE(err.empty());
REQUIRE(out.find(" || ") != std::string::npos);
}
TEST_CASE("transpile_expr: whitelisted math function", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h = {"x"};
std::string out = transpile_expr("math.abs([x])", h, err);
REQUIRE(err.empty());
REQUIRE(out.find("abs(") != std::string::npos);
}
TEST_CASE("transpile_expr: forbidden keyword returns error", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h;
transpile_expr("function() end", h, err);
REQUIRE_FALSE(err.empty());
}
TEST_CASE("transpile_expr: bare identifier not allowed", "[tql_to_sql]") {
std::string err;
std::vector<std::string> h;
transpile_expr("foo", h, err);
REQUIRE_FALSE(err.empty());
}
// ============================================================================
// is_transpilable tests
// ============================================================================
TEST_CASE("is_transpilable: simple expression is transpilable", "[tql_to_sql]") {
std::string err;
bool ok = is_transpilable("[price] * 1.1", err);
REQUIRE(ok);
REQUIRE(err.empty());
}
TEST_CASE("is_transpilable: loop keyword not transpilable", "[tql_to_sql]") {
std::string err;
bool ok = is_transpilable("for i=1,10 do end", err);
REQUIRE_FALSE(ok);
REQUIRE_FALSE(err.empty());
}
// ============================================================================
// emit_sql: select simple
// ============================================================================
TEST_CASE("emit_sql: select simple single table", "[tql_to_sql]") {
State st = make_state_one_stage();
std::vector<TableInput> tables = { make_table("sales", {"name", "amount"}) };
SqlEmit e = emit_sql(st, tables);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("WITH") != std::string::npos);
REQUIRE(e.sql.find("t0") != std::string::npos);
REQUIRE(e.sql.find("\"sales\"") != std::string::npos);
REQUIRE(e.sql.find("\"name\"") != std::string::npos);
REQUIRE(e.sql.find("\"amount\"") != std::string::npos);
REQUIRE(e.sql.find("SELECT * FROM t0") != std::string::npos);
}
TEST_CASE("emit_sql: filter eq produces WHERE with placeholder", "[tql_to_sql]") {
State st = make_state_one_stage();
Filter f;
f.col = 0; // "region"
f.op = Op::Eq;
f.value = "North";
st.stages[0].filters.push_back(f);
std::vector<TableInput> tables = { make_table("sales", {"region", "amount"}) };
SqlEmit e = emit_sql(st, tables);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("WHERE") != std::string::npos);
REQUIRE(e.sql.find(" = ") != std::string::npos);
REQUIRE(e.sql.find("?") != std::string::npos);
REQUIRE(e.params.size() == 1);
REQUIRE(e.params[0] == "North");
}
TEST_CASE("emit_sql: sort produces ORDER BY", "[tql_to_sql]") {
State st = make_state_one_stage();
SortClause sc;
sc.col = "amount";
sc.desc = true;
st.stages[0].sorts.push_back(sc);
std::vector<TableInput> tables = { make_table("sales", {"region", "amount"}) };
SqlEmit e = emit_sql(st, tables);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("ORDER BY") != std::string::npos);
REQUIRE(e.sql.find("DESC") != std::string::npos);
}
TEST_CASE("emit_sql: group by + sum aggregation produces correct SQL", "[tql_to_sql]") {
State st;
// Stage 0: passthrough
st.stages.push_back(Stage{});
// Stage 1: breakout + aggregation
Stage s1;
s1.breakouts.push_back("region");
Aggregation ag;
ag.fn = AggFn::Sum;
ag.col = "amount";
s1.aggregations.push_back(ag);
st.stages.push_back(s1);
st.active_stage = 1;
std::vector<TableInput> tables = { make_table("sales", {"region", "amount"}) };
SqlEmit e = emit_sql(st, tables, 1);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("GROUP BY") != std::string::npos);
REQUIRE(e.sql.find("SUM(") != std::string::npos);
REQUIRE(e.sql.find("\"region\"") != std::string::npos);
REQUIRE(e.sql.find("SELECT * FROM t1") != std::string::npos);
}
TEST_CASE("emit_sql: join two tables produces JOIN clause", "[tql_to_sql]") {
State st = make_state_one_stage();
Join jn;
jn.alias = "cat";
jn.source = "categories";
jn.on.push_back({"cat_id", "id"});
jn.strategy = JoinStrategy::Left;
st.joins.push_back(jn);
st.main_source = "sales";
std::vector<TableInput> tables = {
make_table("sales", {"cat_id", "amount"}),
make_table("categories", {"id", "name"}),
};
SqlEmit e = emit_sql(st, tables);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("LEFT JOIN") != std::string::npos);
REQUIRE(e.sql.find("\"categories\"") != std::string::npos);
REQUIRE(e.sql.find("\"cat\"") != std::string::npos);
REQUIRE(e.sql.find(" ON ") != std::string::npos);
}
TEST_CASE("emit_sql: empty state returns error", "[tql_to_sql]") {
State st; // no stages
std::vector<TableInput> tables = { make_table("t", {"a"}) };
SqlEmit e = emit_sql(st, tables);
REQUIRE_FALSE(e.error.empty());
}
TEST_CASE("emit_sql: empty tables returns error", "[tql_to_sql]") {
State st = make_state_one_stage();
std::vector<TableInput> tables;
SqlEmit e = emit_sql(st, tables);
REQUIRE_FALSE(e.error.empty());
}
TEST_CASE("emit_sql: contains filter produces LIKE with wildcards", "[tql_to_sql]") {
State st = make_state_one_stage();
Filter f;
f.col = 0;
f.op = Op::Contains;
f.value = "foo";
st.stages[0].filters.push_back(f);
std::vector<TableInput> tables = { make_table("t", {"name"}) };
SqlEmit e = emit_sql(st, tables);
REQUIRE(e.error.empty());
REQUIRE(e.sql.find("LIKE") != std::string::npos);
REQUIRE(e.params.size() == 1);
REQUIRE(e.params[0] == "%foo%");
}