a03675113a
- .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>
260 lines
8.5 KiB
C++
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%");
|
|
}
|