// 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 #include 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& 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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%"); }