LCOV - code coverage report
Current view: top level - backend/engine/duckdb/transpiler - transpiler_emit_aggregate_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 300 300
Test Date: 2026-07-02 21:01:18 Functions: 100.0 % 20 20

            Line data    Source code
       1              : #include "backend/engine/duckdb/transpiler/transpiler_test_fixture.h"
       2              : 
       3              : namespace bigquery_emulator {
       4              : namespace backend {
       5              : namespace engine {
       6              : namespace duckdb {
       7              : namespace transpiler {
       8              : 
       9            1 : TEST_F(TranspilerTest, EmitAggregateScanCountStarNoGroupBy) {
      10              :   // `SELECT COUNT(*) FROM people` analyzes to an AggregateScan
      11              :   // with an empty group_by_list and a single aggregate. The
      12              :   // aggregate column gets a synthesized name (`$agg1`); we assert
      13              :   // on it so any drift in the analyzer's naming surfaces here
      14              :   // rather than in the engine integration.
      15            1 :   const ::googlesql::ResolvedStatement* stmt =
      16            1 :       Analyze("SELECT COUNT(*) FROM people");
      17            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      18            1 :   ASSERT_NE(scan, nullptr);
      19            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
      20            1 :   TestTranspiler t;
      21            1 :   EXPECT_EQ(
      22            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>()),
      23            1 :       "SELECT COUNT(*) AS \"$agg1\" FROM (SELECT \"id\", \"name\" FROM "
      24            1 :       "\"people\")");
      25            1 : }
      26              : 
      27            1 : TEST_F(TranspilerTest, EmitAggregateScanSumGroupByColumn) {
      28              :   // SUM over a grouped column threads the column-ref through the
      29              :   // GROUP BY clause and the SELECT list. The grouping column's
      30              :   // ResolvedColumn::name() matches its source name (`id`), so the
      31              :   // AS alias collapses to the column reference.
      32            1 :   const ::googlesql::ResolvedStatement* stmt =
      33            1 :       Analyze("SELECT id, SUM(id) FROM people GROUP BY id");
      34            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      35            1 :   ASSERT_NE(scan, nullptr);
      36            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
      37            1 :   TestTranspiler t;
      38            1 :   EXPECT_EQ(
      39            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>()),
      40            1 :       "SELECT \"id\", SUM(\"id\") AS \"$agg1\" FROM (SELECT \"id\", "
      41            1 :       "\"name\" FROM \"people\") GROUP BY \"id\"");
      42            1 : }
      43              : 
      44            1 : TEST_F(TranspilerTest, EmitAggregateScanAvgMinMaxGroupBy) {
      45              :   // All three of AVG / MIN / MAX share the same emit path; one
      46              :   // test covers the lot. The output column for each aggregate is
      47              :   // again the analyzer-synthesized `$agg<n>` name.
      48            1 :   const ::googlesql::ResolvedStatement* stmt =
      49            1 :       Analyze("SELECT id, AVG(id), MIN(id), MAX(id) FROM people GROUP BY id");
      50            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      51            1 :   ASSERT_NE(scan, nullptr);
      52            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
      53            1 :   TestTranspiler t;
      54            1 :   EXPECT_EQ(
      55            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>()),
      56            1 :       "SELECT \"id\", AVG(\"id\") AS \"$agg1\", MIN(\"id\") AS \"$agg2\", "
      57            1 :       "MAX(\"id\") AS \"$agg3\" FROM (SELECT \"id\", \"name\" FROM "
      58            1 :       "\"people\") GROUP BY \"id\"");
      59            1 : }
      60              : 
      61            1 : TEST_F(TranspilerTest, EmitAggregateScanArrayAggMapsThroughTable) {
      62              :   // `ARRAY_AGG` is in the disposition table (`array_agg: ARRAY_AGG`),
      63              :   // so the lower path emits the DuckDB aggregate verbatim. This
      64              :   // exercises the table-driven dispatch from inside the AggregateScan
      65              :   // emit -- a direct counterpart to `EmitFunctionCallMappedFunction`
      66              :   // above for the aggregate code path.
      67            1 :   const ::googlesql::ResolvedStatement* stmt =
      68            1 :       Analyze("SELECT id, ARRAY_AGG(id) FROM people GROUP BY id");
      69            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      70            1 :   ASSERT_NE(scan, nullptr);
      71            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
      72            1 :   TestTranspiler t;
      73            1 :   EXPECT_EQ(
      74            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>()),
      75            1 :       "SELECT \"id\", if(count(\"id\") < count(*), "
      76            1 :       "error('ARRAY_AGG: input value must be not null'), list(\"id\")) AS "
      77            1 :       "\"$agg1\" FROM (SELECT *, row_number() OVER () AS \"__bq_input_rn\" "
      78            1 :       "FROM (SELECT \"id\", \"name\" FROM \"people\")) GROUP BY \"id\"");
      79            1 : }
      80              : 
      81            1 : TEST_F(TranspilerTest, EmitAggregateScanArrayAggOrderByLimitRewritesToList) {
      82            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
      83            1 :       "SELECT ARRAY_AGG(x ORDER BY x LIMIT 2) FROM UNNEST([3, 1, 2, 4]) AS x");
      84            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      85            1 :   ASSERT_NE(scan, nullptr);
      86            1 :   TestTranspiler t;
      87            1 :   std::string sql =
      88            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
      89            2 :   EXPECT_NE(sql.find("list_slice(list(\"x\" ORDER BY"), std::string::npos)
      90            2 :       << sql;
      91            2 :   EXPECT_NE(sql.find(", 1, 2)"), std::string::npos) << sql;
      92            1 : }
      93              : 
      94            1 : TEST_F(TranspilerTest, EmitAggregateScanStringAggLimitRewritesToArrayToString) {
      95            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
      96            1 :       "SELECT STRING_AGG(fruit, ' & ' LIMIT 2) FROM "
      97            1 :       "UNNEST(['apple', 'pear', 'banana']) AS fruit");
      98            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      99            1 :   ASSERT_NE(scan, nullptr);
     100            1 :   TestTranspiler t;
     101            1 :   std::string sql =
     102            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     103            2 :   EXPECT_NE(sql.find("array_to_string(list_slice(list("), std::string::npos)
     104            2 :       << sql;
     105            2 :   EXPECT_NE(sql.find(", ' & ')"), std::string::npos) << sql;
     106            1 : }
     107              : 
     108            1 : TEST_F(TranspilerTest, EmitAggregateScanStringAggOrderByLimit) {
     109            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     110            1 :       "SELECT STRING_AGG(x, ',' ORDER BY x ASC LIMIT 2) FROM "
     111            1 :       "UNNEST(['c', 'a', 'b']) AS x");
     112            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     113            1 :   ASSERT_NE(scan, nullptr);
     114            1 :   TestTranspiler t;
     115            1 :   std::string sql =
     116            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     117            2 :   EXPECT_NE(sql.find("array_to_string(list_slice(list("), std::string::npos)
     118            2 :       << sql;
     119            2 :   EXPECT_NE(sql.find("ORDER BY \"x\" ASC"), std::string::npos) << sql;
     120            1 : }
     121              : 
     122            1 : TEST_F(TranspilerTest, EmitAggregateScanFallsBackOnUnsupportedAggregate) {
     123              :   // `APPROX_QUANTILES` is on the `unsupported` route per
     124              :   // `docs/ENGINE_POLICY.md`; the aggregate emit returns
     125              :   // "" and the AggregateScan emit propagates the empty string. The
     126              :   // engine surfaces UNIMPLEMENTED for the whole query.
     127            1 :   const ::googlesql::ResolvedStatement* stmt =
     128            1 :       Analyze("SELECT id, APPROX_QUANTILES(id, 2) FROM people GROUP BY id");
     129            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     130            1 :   ASSERT_NE(scan, nullptr);
     131            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
     132            1 :   TestTranspiler t;
     133            1 :   EXPECT_EQ(
     134            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>()),
     135            1 :       "");
     136            1 : }
     137              : 
     138              : // --- GROUPING SETS / ROLLUP / CUBE / GROUPING() ------------------------
     139              : //
     140              : // `docs/ENGINE_POLICY.md` Family 1. Each shape exercises
     141              : // the `grouping_set_list` path in `EmitAggregateScan`:
     142              : //
     143              : //   * Explicit GROUPING SETS  -> `(a, b)`, `(a)`, `()` entries.
     144              : //   * ROLLUP                  -> single ROLLUP entry expands to N+1
     145              : //                                grouping sets via `ResolvedRollup`.
     146              : //   * CUBE                    -> single CUBE entry expands to 2^N
     147              : //                                grouping sets via `ResolvedCube`.
     148              : //   * GROUPING(<col>)         -> `ResolvedGroupingCall` in
     149              : //                                `grouping_call_list`; projects as
     150              : //                                `GROUPING(<col>) AS "<output>"`.
     151              : 
     152            1 : TEST_F(TranspilerTest, EmitAggregateScanGroupingSetsExplicit) {
     153              :   // `GROUP BY GROUPING SETS ((id, name), (id), ())` analyzes with
     154              :   // three `ResolvedGroupingSet` entries in `grouping_set_list`.
     155              :   // The emit lands as `GROUP BY GROUPING SETS ((<a>, <b>), (<a>),
     156              :   // ())`, with each grouping-set tuple referencing the SELECT-list
     157              :   // aliases for `group_by_list` columns. DuckDB resolves the alias
     158              :   // inside GROUP BY GROUPING SETS, so the emitted SQL is
     159              :   // self-contained.
     160            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     161            1 :       "SELECT id, name, COUNT(*) FROM people "
     162            1 :       "GROUP BY GROUPING SETS ((id, name), (id), ())");
     163            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     164            1 :   ASSERT_NE(scan, nullptr);
     165            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
     166            1 :   TestTranspiler t;
     167            1 :   std::string sql =
     168            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     169            2 :   EXPECT_NE(sql.find(" GROUP BY GROUPING SETS ("), std::string::npos)
     170            2 :       << "expected GROUP BY GROUPING SETS keyword; got: " << sql;
     171            2 :   EXPECT_NE(sql.find("(\"id\", \"name\")"), std::string::npos)
     172            2 :       << "expected (id, name) tuple; got: " << sql;
     173            2 :   EXPECT_NE(sql.find("(\"id\")"), std::string::npos)
     174            2 :       << "expected (id) tuple; got: " << sql;
     175            2 :   EXPECT_NE(sql.find("()"), std::string::npos)
     176            2 :       << "expected empty () tuple; got: " << sql;
     177            1 : }
     178              : 
     179            1 : TEST_F(TranspilerTest, EmitAggregateScanRollupAnalyzerExpandsToSets) {
     180              :   // The analyzer canonicalizes `GROUP BY ROLLUP(id, name)` into
     181              :   // three `ResolvedGroupingSet` entries -- `(id, name), (id), ()`
     182              :   // -- in `grouping_set_list`, not a single `ResolvedRollup`. Pin
     183              :   // the expanded form here so the user-visible BigQuery semantics
     184              :   // (one row per ROLLUP step) are exercised end-to-end on the
     185              :   // DuckDB fast path. The `ResolvedRollup` direct-construction
     186              :   // test below pins the keyword emit code path for shapes that
     187              :   // upstream builders could produce.
     188            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     189            1 :       "SELECT id, name, COUNT(*) FROM people GROUP BY ROLLUP(id, name)");
     190            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     191            1 :   ASSERT_NE(scan, nullptr);
     192            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
     193            1 :   TestTranspiler t;
     194            1 :   std::string sql =
     195            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     196            2 :   EXPECT_NE(sql.find("GROUP BY GROUPING SETS ("), std::string::npos)
     197            2 :       << "expected GROUP BY GROUPING SETS keyword; got: " << sql;
     198            2 :   EXPECT_NE(sql.find("(\"id\", \"name\")"), std::string::npos)
     199            2 :       << "expected (id, name) leaf; got: " << sql;
     200            2 :   EXPECT_NE(sql.find("(\"id\")"), std::string::npos)
     201            2 :       << "expected (id) subtotal; got: " << sql;
     202            2 :   EXPECT_NE(sql.find("()"), std::string::npos)
     203            2 :       << "expected () grand-total tuple; got: " << sql;
     204            1 : }
     205              : 
     206            1 : TEST_F(TranspilerTest, EmitAggregateScanCubeAnalyzerExpandsToSets) {
     207              :   // CUBE(id, name) canonicalizes to 2^2 = 4 grouping sets:
     208              :   // `(id, name), (id), (name), ()`. Same shape as the ROLLUP test
     209              :   // above; here we pin the full fan-out so a partial-emit
     210              :   // regression in `EmitGroupingSetEntry` shows up as a missing
     211              :   // tuple.
     212            1 :   const ::googlesql::ResolvedStatement* stmt =
     213            1 :       Analyze("SELECT id, name, COUNT(*) FROM people GROUP BY CUBE(id, name)");
     214            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     215            1 :   ASSERT_NE(scan, nullptr);
     216            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
     217            1 :   TestTranspiler t;
     218            1 :   std::string sql =
     219            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     220            2 :   EXPECT_NE(sql.find("(\"id\", \"name\")"), std::string::npos) << sql;
     221            2 :   EXPECT_NE(sql.find("(\"id\")"), std::string::npos) << sql;
     222            2 :   EXPECT_NE(sql.find("(\"name\")"), std::string::npos) << sql;
     223            2 :   EXPECT_NE(sql.find("()"), std::string::npos) << sql;
     224            1 : }
     225              : 
     226              : // --- PIVOT / UNPIVOT -----------------------------------------------------
     227              : //
     228              : // `EmitPivotScan` lowers a BigQuery `PIVOT(<agg>(...) FOR <expr> IN
     229              : // (<vals>))` to DuckDB conditional aggregation using `FILTER`. Each
     230              : // `ResolvedPivotColumn` carries the (pivot_expr_index,
     231              : // pivot_value_index) tuple plus the analyzer-chosen output column
     232              : // name, which the emit aliases onto a `<agg> FILTER (WHERE
     233              : // <for_expr> = <pivot_value>)` projection.
     234              : //
     235              : // `EmitUnpivotScan` lowers `UNPIVOT(<value_cols> FOR <label_col> IN
     236              : // (<arg_groups>))` to a UNION ALL of per-arg SELECTs. Each branch
     237              : // projects the input columns that pass through unchanged, renames
     238              : // the arg's column refs to the value-column names, and adds the
     239              : // arg's label literal under `label_column`. When `include_nulls()`
     240              : // is false (the BigQuery default), each branch adds a
     241              : // `WHERE NOT (val0 IS NULL AND ... AND valN IS NULL)` filter so the
     242              : // EXCLUDE NULLS semantics match BigQuery.
     243              : 
     244            1 : TEST_F(TranspilerTest, EmitPivotScanBuildsFilterAggregates) {
     245              :   // `SUM(amount) FOR kind IN ('A', 'B')` yields one `ResolvedPivotColumn`
     246              :   // per (pivot_expr_index=0, pivot_value_index=v) pair. The emit
     247              :   // projects each as `SUM("amount") FILTER (WHERE "kind" = '<v>')`
     248              :   // aliased to the analyzer-chosen output column name.
     249            1 :   const ::googlesql::ResolvedStatement* stmt =
     250            1 :       Analyze("SELECT * FROM sales PIVOT(SUM(amount) FOR kind IN ('A', 'B'))");
     251            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     252            1 :   ASSERT_NE(scan, nullptr);
     253            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_PIVOT_SCAN);
     254            1 :   TestTranspiler t;
     255            1 :   std::string sql =
     256            1 :       t.EmitPivotScan(scan->GetAs<::googlesql::ResolvedPivotScan>());
     257            2 :   EXPECT_NE(sql.find("SUM(\"amount\") FILTER (WHERE \"kind\" = 'A')"),
     258            2 :             std::string::npos)
     259            2 :       << "expected SUM FILTER for 'A'; got: " << sql;
     260            2 :   EXPECT_NE(sql.find("SUM(\"amount\") FILTER (WHERE \"kind\" = 'B')"),
     261            2 :             std::string::npos)
     262            2 :       << "expected SUM FILTER for 'B'; got: " << sql;
     263            2 :   EXPECT_NE(sql.find(" GROUP BY \"region\""), std::string::npos)
     264            2 :       << "expected GROUP BY on the pass-through grouping column; got: " << sql;
     265            1 : }
     266              : 
     267            1 : TEST_F(TranspilerTest, EmitPivotScanMultipleAggregates) {
     268              :   // Two pivot expressions x two pivot values = four output columns.
     269              :   // The analyzer reuses one pivot_expr_list / pivot_value_list and
     270              :   // tags each output column with (expr_index, value_index); the emit
     271              :   // pulls the cached aggregate SQL for each index and pairs it with
     272              :   // the matching value's filter clause.
     273            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     274            1 :       "SELECT * FROM sales "
     275            1 :       "PIVOT(SUM(amount) AS s, COUNT(*) AS c FOR kind IN ('A', 'B'))");
     276            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     277            1 :   ASSERT_NE(scan, nullptr);
     278            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_PIVOT_SCAN);
     279            1 :   TestTranspiler t;
     280            1 :   std::string sql =
     281            1 :       t.EmitPivotScan(scan->GetAs<::googlesql::ResolvedPivotScan>());
     282            2 :   EXPECT_NE(sql.find("SUM(\"amount\") FILTER (WHERE \"kind\" = 'A')"),
     283            2 :             std::string::npos)
     284            2 :       << sql;
     285            2 :   EXPECT_NE(sql.find("SUM(\"amount\") FILTER (WHERE \"kind\" = 'B')"),
     286            2 :             std::string::npos)
     287            2 :       << sql;
     288            2 :   EXPECT_NE(sql.find("COUNT(*) FILTER (WHERE \"kind\" = 'A')"),
     289            2 :             std::string::npos)
     290            2 :       << sql;
     291            2 :   EXPECT_NE(sql.find("COUNT(*) FILTER (WHERE \"kind\" = 'B')"),
     292            2 :             std::string::npos)
     293            2 :       << sql;
     294            1 : }
     295              : 
     296            1 : TEST_F(TranspilerTest, EmitUnpivotScanExcludeNullsByDefault) {
     297              :   // BigQuery's default UNPIVOT semantic (EXCLUDE NULLS). The emit
     298              :   // expands each input row via CROSS JOIN LATERAL (VALUES ...); the
     299              :   // outer WHERE filters out rows where every value column is NULL.
     300              :   // The label column is the analyzer's string label_list[i].
     301            1 :   const ::googlesql::ResolvedStatement* stmt =
     302            1 :       Analyze("SELECT * FROM wide UNPIVOT(value FOR quarter IN (q1, q2))");
     303            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     304            1 :   ASSERT_NE(scan, nullptr);
     305            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_UNPIVOT_SCAN);
     306            1 :   TestTranspiler t;
     307            1 :   std::string sql =
     308            1 :       t.EmitUnpivotScan(scan->GetAs<::googlesql::ResolvedUnpivotScan>());
     309            2 :   EXPECT_NE(sql.find(" CROSS JOIN LATERAL (VALUES "), std::string::npos)
     310            2 :       << "expected LATERAL VALUES unpivot expansion; got: " << sql;
     311            2 :   EXPECT_NE(sql.find("'q1'"), std::string::npos)
     312            2 :       << "expected q1 label in VALUES tuple; got: " << sql;
     313            2 :   EXPECT_NE(sql.find("'q2'"), std::string::npos)
     314            2 :       << "expected q2 label in VALUES tuple; got: " << sql;
     315            2 :   EXPECT_NE(sql.find("WHERE NOT (u.\"value\" IS NULL)"), std::string::npos)
     316            2 :       << "expected EXCLUDE NULLS filter; got: " << sql;
     317            1 : }
     318              : 
     319            1 : TEST_F(TranspilerTest, EmitWithScanRecursiveHierarchyTraversal) {
     320              :   // Mirrors `conformance/fixtures/advanced_relational/recursive_cte.yaml`:
     321              :   // anchor arm projects a literal depth column (`0 AS depth`) whose
     322              :   // analyzer name is a synthesized `$colN`; the recursive arm joins
     323              :   // the CTE ref back to `org` and increments depth.
     324            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     325            1 :       "WITH RECURSIVE descents AS ("
     326            1 :       "  SELECT employee, 0 AS depth FROM org WHERE manager IS NULL"
     327            1 :       "  UNION ALL"
     328            1 :       "  SELECT org.employee, depth + 1"
     329            1 :       "    FROM descents JOIN org AS org ON org.manager = descents.employee"
     330            1 :       ")"
     331            1 :       "SELECT employee, depth FROM descents ORDER BY depth, employee");
     332            1 :   ASSERT_NE(stmt, nullptr);
     333            1 :   TestTranspiler t;
     334            1 :   std::string sql = t.Transpile(stmt);
     335            2 :   EXPECT_NE(sql.find("WITH RECURSIVE \"descents\""), std::string::npos) << sql;
     336            2 :   EXPECT_NE(sql.find("\"_cte_0\""), std::string::npos) << sql;
     337            2 :   EXPECT_NE(sql.find("\"_cte_1\""), std::string::npos) << sql;
     338            2 :   EXPECT_NE(sql.find(" UNION ALL "), std::string::npos) << sql;
     339            2 :   EXPECT_NE(sql.find("FROM \"descents\""), std::string::npos) << sql;
     340            2 :   EXPECT_NE(sql.find("\"employee\" AS \"_cte_0\""), std::string::npos) << sql;
     341            2 :   EXPECT_NE(sql.find("\"depth\" AS \"_cte_1\""), std::string::npos) << sql;
     342            1 : }
     343              : 
     344            1 : TEST_F(TranspilerTest, EmitWithScanRecursiveLowersToWithRecursive) {
     345              :   // `docs/ENGINE_POLICY.md` Family 4. A `WITH
     346              :   // RECURSIVE t AS (SELECT 1 AS n UNION ALL SELECT n FROM t) ...`
     347              :   // lowers to DuckDB's `WITH RECURSIVE`. The transpiler stages a
     348              :   // per-CTE context with stable anchor column names (`_cte_0`),
     349              :   // renames each arm's output columns onto those anchors, and
     350              :   // unions them; the recursive-arm `ResolvedRecursiveRefScan`
     351              :   // projects from the anchor names back to the analyzer's per-ref
     352              :   // column names.
     353              :   //
     354              :   // The recursive arm here is just `SELECT n FROM t` (no
     355              :   // `WHERE ... < ...`) so the test does not depend on the `$less`
     356              :   // operator's disposition entry, which is owned by a later
     357              :   // function-dispatch plan.
     358            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     359            1 :       "WITH RECURSIVE t AS ("
     360            1 :       "  SELECT 1 AS n"
     361            1 :       "  UNION ALL"
     362            1 :       "  SELECT n FROM t"
     363            1 :       ")"
     364            1 :       "SELECT n FROM t");
     365            1 :   ASSERT_NE(stmt, nullptr);
     366            1 :   TestTranspiler t;
     367            1 :   std::string sql = t.Transpile(stmt);
     368            2 :   EXPECT_NE(sql.find("WITH RECURSIVE \"t\""), std::string::npos)
     369            2 :       << "expected WITH RECURSIVE keyword; got: " << sql;
     370            2 :   EXPECT_NE(sql.find("\"_cte_0\""), std::string::npos)
     371            2 :       << "expected anchor column name; got: " << sql;
     372            2 :   EXPECT_NE(sql.find(" UNION ALL "), std::string::npos)
     373            2 :       << "expected UNION ALL between anchor and recursive arm; got: " << sql;
     374            2 :   EXPECT_NE(sql.find("FROM \"t\""), std::string::npos)
     375            2 :       << "expected recursive ref to read FROM the CTE; got: " << sql;
     376            1 : }
     377              : 
     378            1 : TEST_F(TranspilerTest, EmitUnpivotScanIncludeNullsSkipsFilter) {
     379              :   // `INCLUDE NULLS` flips `include_nulls()` true; the outer query
     380              :   // should not carry the WHERE filter.
     381            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     382            1 :       "SELECT * FROM wide "
     383            1 :       "UNPIVOT INCLUDE NULLS (value FOR quarter IN (q1, q2))");
     384            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     385            1 :   ASSERT_NE(scan, nullptr);
     386            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_UNPIVOT_SCAN);
     387            1 :   TestTranspiler t;
     388            1 :   std::string sql =
     389            1 :       t.EmitUnpivotScan(scan->GetAs<::googlesql::ResolvedUnpivotScan>());
     390            2 :   EXPECT_EQ(sql.find("WHERE NOT"), std::string::npos)
     391            2 :       << "INCLUDE NULLS should omit the WHERE filter; got: " << sql;
     392            2 :   EXPECT_NE(sql.find(" CROSS JOIN LATERAL (VALUES "), std::string::npos) << sql;
     393            1 : }
     394              : 
     395            1 : TEST_F(TranspilerTest, EmitAggregateScanGroupingCallProjectsBitMask) {
     396              :   // `SELECT GROUPING(id), ... FROM ... GROUP BY ROLLUP(id, name)`
     397              :   // creates a `ResolvedGroupingCall` in `grouping_call_list`. The
     398              :   // emit projects it as `GROUPING("id") AS "<output>"` so the
     399              :   // SELECT list exposes the bit at the analyzer-chosen output
     400              :   // column name.
     401            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     402            1 :       "SELECT id, name, GROUPING(id), COUNT(*) FROM people "
     403            1 :       "GROUP BY ROLLUP(id, name)");
     404            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     405            1 :   ASSERT_NE(scan, nullptr);
     406            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_AGGREGATE_SCAN);
     407            1 :   TestTranspiler t;
     408            1 :   std::string sql =
     409            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     410            2 :   EXPECT_NE(sql.find("GROUPING(\"id\") AS "), std::string::npos)
     411            2 :       << "expected GROUPING projection; got: " << sql;
     412            1 : }
     413              : 
     414            1 : TEST_F(TranspilerTest, EmitAggregateScanSumFilterClause) {
     415            1 :   const ::googlesql::ResolvedStatement* stmt =
     416            1 :       Analyze("SELECT SUM(amount WHERE kind = 'A') FROM sales");
     417            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     418            1 :   ASSERT_NE(scan, nullptr);
     419            1 :   TestTranspiler t;
     420            1 :   std::string sql =
     421            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     422            2 :   EXPECT_NE(sql.find("SUM(\"amount\") FILTER (WHERE"), std::string::npos)
     423            2 :       << sql;
     424            2 :   EXPECT_NE(sql.find("\"kind\" = 'A'"), std::string::npos) << sql;
     425            1 : }
     426              : 
     427            1 : TEST_F(TranspilerTest, EmitAggregateScanArrayAggIgnoreNulls) {
     428            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     429            1 :       "SELECT ARRAY_AGG(x IGNORE NULLS) FROM UNNEST([1, NULL, 2]) AS x");
     430            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     431            1 :   ASSERT_NE(scan, nullptr);
     432            1 :   TestTranspiler t;
     433            1 :   std::string sql =
     434            1 :       t.EmitAggregateScan(scan->GetAs<::googlesql::ResolvedAggregateScan>());
     435            2 :   EXPECT_NE(sql.find("FILTER (WHERE \"x\" IS NOT NULL)"), std::string::npos)
     436            2 :       << sql;
     437            1 : }
     438              : 
     439              : // --- Order By -----------------------------------------------------------
     440              : 
     441              : }  // namespace transpiler
     442              : }  // namespace duckdb
     443              : }  // namespace engine
     444              : }  // namespace backend
     445              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1