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

            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, EmitSetOperationScanUnionAll) {
      10              :   // BigQuery `<lhs> UNION ALL <rhs>` analyzes to a
      11              :   // `ResolvedSetOperationScan` whose `op_type=UNION_ALL`,
      12              :   // `column_match_mode=BY_POSITION`, and two
      13              :   // `ResolvedSetOperationItem`s. GoogleSQL takes the parent
      14              :   // column's name from the leftmost input's column (`id` here), so
      15              :   // the LHS item's projection collapses (`"id"` -> `"id"`) and the
      16              :   // RHS item renames `order_id` to `id` to land on the parent's
      17              :   // column name.
      18              :   //
      19              :   // The analyzer wraps each `SELECT <col> FROM <table>` arm in a
      20              :   // ResolvedProjectScan over the ResolvedTableScan (because the
      21              :   // selected columns are a subset of the table's full column list),
      22              :   // which is why the per-arm SQL has the extra `(SELECT ... FROM
      23              :   // (SELECT ... FROM ...))` nesting -- the outer SELECT is the
      24              :   // set-op item's projection, the middle SELECT is the analyzer's
      25              :   // ProjectScan, and the inner SELECT is the TableScan.
      26            1 :   const ::googlesql::ResolvedStatement* stmt =
      27            1 :       Analyze("SELECT id FROM people UNION ALL SELECT order_id FROM orders");
      28            1 :   ASSERT_NE(stmt, nullptr);
      29            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      30            1 :   ASSERT_NE(scan, nullptr);
      31            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
      32            1 :   TestTranspiler t;
      33            1 :   EXPECT_EQ(t.EmitSetOperationScan(
      34            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
      35            1 :             "SELECT \"id\" FROM (SELECT *, 1 AS \"__bq_union_ord\" FROM "
      36            1 :             "(SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", "
      37            1 :             "\"name\" FROM \"people\"))) UNION ALL SELECT *, 2 AS "
      38            1 :             "\"__bq_union_ord\" FROM (SELECT \"order_id\" AS \"id\" FROM "
      39            1 :             "(SELECT \"order_id\" FROM (SELECT \"order_id\", \"amount\" FROM "
      40            1 :             "\"orders\")))) ORDER BY \"__bq_union_ord\"");
      41            1 : }
      42              : 
      43            1 : TEST_F(TranspilerTest, EmitSetOperationScanUnionDistinct) {
      44              :   // `UNION DISTINCT` lowers to DuckDB's bare `UNION` (DuckDB's
      45              :   // default duplicate-handling on `UNION` is DISTINCT, matching the
      46              :   // BigQuery semantics). The per-item projection shape is the same
      47              :   // as UNION ALL because the duplicate-handling is the only
      48              :   // difference between the two ops.
      49            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
      50            1 :       "SELECT id FROM people UNION DISTINCT SELECT order_id FROM orders");
      51            1 :   ASSERT_NE(stmt, nullptr);
      52            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      53            1 :   ASSERT_NE(scan, nullptr);
      54            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
      55            1 :   TestTranspiler t;
      56            1 :   EXPECT_EQ(t.EmitSetOperationScan(
      57            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
      58            1 :             "SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", \"name\" "
      59            1 :             "FROM \"people\"))"
      60            1 :             " UNION "
      61            1 :             "SELECT \"order_id\" AS \"id\" FROM (SELECT \"order_id\" FROM "
      62            1 :             "(SELECT \"order_id\", \"amount\" FROM \"orders\"))");
      63            1 : }
      64              : 
      65            1 : TEST_F(TranspilerTest, EmitSetOperationScanIntersectDistinct) {
      66              :   // `INTERSECT DISTINCT` lowers to DuckDB's bare `INTERSECT` (also
      67              :   // DISTINCT by default). Same item shape as UNION; only the
      68              :   // keyword between items changes. The output column name comes
      69              :   // from the leftmost input's column.
      70            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
      71            1 :       "SELECT id FROM people INTERSECT DISTINCT SELECT order_id FROM orders");
      72            1 :   ASSERT_NE(stmt, nullptr);
      73            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      74            1 :   ASSERT_NE(scan, nullptr);
      75            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
      76            1 :   TestTranspiler t;
      77            1 :   EXPECT_EQ(t.EmitSetOperationScan(
      78            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
      79            1 :             "SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", \"name\" "
      80            1 :             "FROM \"people\"))"
      81            1 :             " INTERSECT "
      82            1 :             "SELECT \"order_id\" AS \"id\" FROM (SELECT \"order_id\" FROM "
      83            1 :             "(SELECT \"order_id\", \"amount\" FROM \"orders\"))");
      84            1 : }
      85              : 
      86            1 : TEST_F(TranspilerTest, EmitSetOperationScanExceptDistinct) {
      87              :   // `EXCEPT DISTINCT` lowers to DuckDB's bare `EXCEPT` (DISTINCT by
      88              :   // default). BigQuery's EXCEPT DISTINCT semantics (row R in LHS at
      89              :   // least once and absent from RHS) match DuckDB's bag-difference
      90              :   // followed by DISTINCT.
      91            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
      92            1 :       "SELECT id FROM people EXCEPT DISTINCT SELECT order_id FROM orders");
      93            1 :   ASSERT_NE(stmt, nullptr);
      94            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
      95            1 :   ASSERT_NE(scan, nullptr);
      96            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
      97            1 :   TestTranspiler t;
      98            1 :   EXPECT_EQ(t.EmitSetOperationScan(
      99            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
     100            1 :             "SELECT * FROM (SELECT \"id\" FROM (SELECT \"id\" FROM "
     101            1 :             "(SELECT \"id\", \"name\" FROM \"people\")) EXCEPT SELECT "
     102            1 :             "\"order_id\" AS \"id\" FROM (SELECT \"order_id\" FROM "
     103            1 :             "(SELECT \"order_id\", \"amount\" FROM \"orders\"))) ORDER BY 1");
     104            1 : }
     105              : 
     106            1 : TEST_F(TranspilerTest, EmitSetOperationScanIdenticalArmsBothCollapse) {
     107              :   // When both arms expose the same column name as the parent's
     108              :   // output column, the per-item AS aliases collapse on both sides.
     109              :   // Identical-arm `SELECT id FROM people UNION ALL SELECT id FROM
     110              :   // people` is the smallest input that exercises the both-side
     111              :   // collapse path -- both items project `"id"` onto the parent's
     112              :   // `id` column, so neither projection needs an AS keyword. This
     113              :   // pins the symmetric-collapse path that the per-arm-rename tests
     114              :   // above leave only half-covered.
     115            1 :   const ::googlesql::ResolvedStatement* stmt =
     116            1 :       Analyze("SELECT id FROM people UNION ALL SELECT id FROM people");
     117            1 :   ASSERT_NE(stmt, nullptr);
     118            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     119            1 :   ASSERT_NE(scan, nullptr);
     120            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     121            1 :   TestTranspiler t;
     122            1 :   EXPECT_EQ(t.EmitSetOperationScan(
     123            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
     124            1 :             "SELECT \"id\" FROM (SELECT *, 1 AS \"__bq_union_ord\" FROM "
     125            1 :             "(SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", "
     126            1 :             "\"name\" FROM \"people\"))) UNION ALL SELECT *, 2 AS "
     127            1 :             "\"__bq_union_ord\" FROM (SELECT \"id\" FROM (SELECT \"id\" FROM "
     128            1 :             "(SELECT \"id\", \"name\" FROM \"people\")))) ORDER BY "
     129            1 :             "\"__bq_union_ord\"");
     130            1 : }
     131              : 
     132            1 : TEST_F(TranspilerTest, EmitSetOperationScanMultiColumnPreservesOrder) {
     133              :   // Two-column UNION ALL. The LHS exposes `id, name`; the RHS
     134              :   // (`SELECT order_id, CAST(amount AS STRING) FROM orders`)
     135              :   // renames both columns to land on the LHS-named output columns
     136              :   // (`id`, `name`). The test pins that the per-item projections
     137              :   // honor positional column matching even when each column needs a
     138              :   // different rename direction.
     139              :   //
     140              :   // The analyzer assigns column IDs across the whole query and
     141              :   // hands the synthesized computed-column name through them; the
     142              :   // CAST in the RHS lands as `$col2` (slot 2 in the overall
     143              :   // computed-column ordering), not `$col1`. The set-op item
     144              :   // renames both onto the parent's `id` / `name`.
     145            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     146            1 :       "SELECT id, name FROM people UNION ALL "
     147            1 :       "SELECT order_id, CAST(amount AS STRING) FROM orders");
     148            1 :   ASSERT_NE(stmt, nullptr);
     149            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     150            1 :   ASSERT_NE(scan, nullptr);
     151            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     152            1 :   TestTranspiler t;
     153            1 :   EXPECT_EQ(
     154            1 :       t.EmitSetOperationScan(
     155            1 :           scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
     156            1 :       "SELECT \"id\", \"name\" FROM (SELECT *, 1 AS \"__bq_union_ord\" FROM "
     157            1 :       "(SELECT \"id\", \"name\" FROM (SELECT \"id\", \"name\" FROM "
     158            1 :       "\"people\")) UNION ALL SELECT *, 2 AS \"__bq_union_ord\" FROM "
     159            1 :       "(SELECT \"order_id\" AS \"id\", \"$col2\" AS \"name\" FROM (SELECT "
     160            1 :       "\"order_id\", CAST(\"amount\" AS VARCHAR) AS \"$col2\" FROM (SELECT "
     161            1 :       "\"order_id\", \"amount\" FROM \"orders\")))) ORDER BY "
     162            1 :       "\"__bq_union_ord\"");
     163            1 : }
     164              : 
     165            1 : TEST_F(TranspilerTest, EmitSetOperationScanThreeArmFlattening) {
     166              :   // BigQuery / GoogleSQL flattens same-op chains so a three-way
     167              :   // `UNION ALL` lands as a single `ResolvedSetOperationScan` with
     168              :   // three items, not a tree. The emit joins all three items with
     169              :   // the keyword.
     170            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     171            1 :       "SELECT id FROM people UNION ALL SELECT order_id FROM orders "
     172            1 :       "UNION ALL SELECT amount FROM orders");
     173            1 :   ASSERT_NE(stmt, nullptr);
     174            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     175            1 :   ASSERT_NE(scan, nullptr);
     176            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     177            1 :   const auto* set_op = scan->GetAs<::googlesql::ResolvedSetOperationScan>();
     178            1 :   ASSERT_EQ(set_op->input_item_list_size(), 3);
     179            1 :   TestTranspiler t;
     180            1 :   EXPECT_EQ(t.EmitSetOperationScan(set_op),
     181            1 :             "SELECT \"id\" FROM (SELECT *, 1 AS \"__bq_union_ord\" FROM "
     182            1 :             "(SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", "
     183            1 :             "\"name\" FROM \"people\"))) UNION ALL SELECT *, 2 AS "
     184            1 :             "\"__bq_union_ord\" FROM (SELECT \"order_id\" AS \"id\" FROM "
     185            1 :             "(SELECT \"order_id\" FROM (SELECT \"order_id\", \"amount\" FROM "
     186            1 :             "\"orders\"))) UNION ALL SELECT *, 3 AS \"__bq_union_ord\" FROM "
     187            1 :             "(SELECT \"amount\" AS \"id\" FROM (SELECT \"amount\" FROM "
     188            1 :             "(SELECT \"order_id\", \"amount\" FROM \"orders\")))) ORDER BY "
     189            1 :             "\"__bq_union_ord\"");
     190            1 : }
     191              : 
     192            1 : TEST_F(TranspilerTest, EmitSetOperationScanNestedDifferentOps) {
     193              :   // Mixing operators (UNION ALL outside, INTERSECT DISTINCT inside)
     194              :   // forces the analyzer to nest: the outer UNION ALL has one
     195              :   // TableScan-y item plus one SetOperationScan item. The emit
     196              :   // composes recursively -- each item's child scan goes through
     197              :   // `EmitScan`, which dispatches back to `EmitSetOperationScan`
     198              :   // for the inner set-op.
     199            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     200            1 :       "SELECT id FROM people UNION ALL "
     201            1 :       "(SELECT order_id FROM orders INTERSECT DISTINCT "
     202            1 :       "SELECT amount FROM orders)");
     203            1 :   ASSERT_NE(stmt, nullptr);
     204            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     205            1 :   ASSERT_NE(scan, nullptr);
     206            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     207            1 :   TestTranspiler t;
     208              :   // The inner INTERSECT's parent column name is `order_id` (the
     209              :   // leftmost input's column), and the outer UNION ALL renames it
     210              :   // onto its own parent column `id` for the second arm.
     211            1 :   EXPECT_EQ(t.EmitSetOperationScan(
     212            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
     213            1 :             "SELECT \"id\" FROM (SELECT *, 1 AS \"__bq_union_ord\" FROM "
     214            1 :             "(SELECT \"id\" FROM (SELECT \"id\" FROM (SELECT \"id\", "
     215            1 :             "\"name\" FROM \"people\"))) UNION ALL SELECT *, 2 AS "
     216            1 :             "\"__bq_union_ord\" FROM (SELECT \"order_id\" AS \"id\" FROM "
     217            1 :             "(SELECT \"order_id\" FROM (SELECT \"order_id\" FROM (SELECT "
     218            1 :             "\"order_id\", \"amount\" FROM \"orders\")) INTERSECT SELECT "
     219            1 :             "\"amount\" AS \"order_id\" FROM (SELECT \"amount\" FROM (SELECT "
     220            1 :             "\"order_id\", \"amount\" FROM \"orders\"))))) ORDER BY "
     221            1 :             "\"__bq_union_ord\"");
     222            1 : }
     223              : 
     224            1 : TEST_F(TranspilerTest, EmitSetOperationScanFallsBackOnUnloweredChild) {
     225              :   // If any child scan returns "" the whole set-op emit must
     226              :   // propagate the empty string. `BIT_COUNT` is on the
     227              :   // `semantic_executor` route in the YAML disposition table so the
     228              :   // right-hand ProjectScan's computed column emit returns "" ->
     229              :   // ProjectScan returns "" -> set-op item returns "" -> set-op
     230              :   // scan returns "".
     231            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     232            1 :       "SELECT id FROM people UNION ALL SELECT BIT_COUNT(amount) FROM orders");
     233            1 :   ASSERT_NE(stmt, nullptr);
     234            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     235            1 :   ASSERT_NE(scan, nullptr);
     236            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     237            1 :   TestTranspiler t;
     238            1 :   EXPECT_EQ(t.EmitSetOperationScan(
     239            1 :                 scan->GetAs<::googlesql::ResolvedSetOperationScan>()),
     240            1 :             "");
     241            1 : }
     242              : 
     243            1 : TEST_F(TranspilerTest, EmitSetOperationScanUnionAllDuplicateBehaviorContrast) {
     244              :   // Execution-style contrast between UNION ALL and UNION DISTINCT
     245              :   // on the *same* input shape. We assert on the SQL strings (this
     246              :   // fixture does not have a running DuckDB connection) so a
     247              :   // regression in either keyword choice surfaces here. The
     248              :   // expected duplicate behavior is documented in
     249              :   // `ResolvedSetOperationScan`'s comment block in
     250              :   // `resolved_ast.h` (UNION ALL keeps all rows, UNION DISTINCT
     251              :   // dedupes); DuckDB's `UNION ALL` and `UNION` (DISTINCT by
     252              :   // default) match that contract.
     253              :   //
     254              :   // We compute each side's SQL fully and discard the analyzer's
     255              :   // output before re-`Analyze`-ing for the next side. The fixture
     256              :   // `last_output_` slot is single-shot (the second `Analyze` call
     257              :   // would otherwise free the first AST out from under us), so the
     258              :   // strings are the durable artifact we compare across the two
     259              :   // emits.
     260            1 :   std::string sql_all;
     261            1 :   {
     262            1 :     const ::googlesql::ResolvedStatement* stmt =
     263            1 :         Analyze("SELECT id FROM people UNION ALL SELECT id FROM people");
     264            1 :     ASSERT_NE(stmt, nullptr);
     265            1 :     const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     266            1 :     ASSERT_NE(scan, nullptr);
     267            1 :     ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     268            1 :     TestTranspiler t;
     269            1 :     sql_all = t.EmitSetOperationScan(
     270            1 :         scan->GetAs<::googlesql::ResolvedSetOperationScan>());
     271            1 :   }
     272            0 :   std::string sql_distinct;
     273            1 :   {
     274            1 :     const ::googlesql::ResolvedStatement* stmt =
     275            1 :         Analyze("SELECT id FROM people UNION DISTINCT SELECT id FROM people");
     276            1 :     ASSERT_NE(stmt, nullptr);
     277            1 :     const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     278            1 :     ASSERT_NE(scan, nullptr);
     279            1 :     ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     280            1 :     TestTranspiler t;
     281            1 :     sql_distinct = t.EmitSetOperationScan(
     282            1 :         scan->GetAs<::googlesql::ResolvedSetOperationScan>());
     283            1 :   }
     284            0 :   EXPECT_NE(sql_all, sql_distinct);
     285            1 :   EXPECT_NE(sql_all.find(" UNION ALL "), std::string::npos);
     286            1 :   EXPECT_EQ(sql_all.find(" UNION DISTINCT "), std::string::npos);
     287            1 :   EXPECT_EQ(sql_distinct.find(" UNION ALL "), std::string::npos);
     288              :   // The bare ` UNION ` keyword (with spaces on both sides) needs
     289              :   // to land in the distinct emit; we deliberately do NOT match a
     290              :   // substring `UNION` because that would also match `UNION ALL`.
     291            1 :   EXPECT_NE(sql_distinct.find(" UNION "), std::string::npos);
     292            1 : }
     293              : 
     294              : // Helper: synthesize a `ResolvedSetOperationScan` directly so the
     295              : // fallback paths can be exercised without depending on the
     296              : // analyzer producing the matching surface SQL. Each "arm" of the
     297              : // set operation is a fresh `ResolvedSingleRowScan` so the inner
     298              : // emit composes onto `SELECT 1`. The parent's `column_list` is
     299              : // left empty so the per-item projection lands on `SELECT *`,
     300              : // which keeps the fallback assertions focused on the
     301              : // kind/mode-bail behavior of `EmitSetOperationScan` rather than
     302              : // the per-item projection logic.
     303              : std::unique_ptr<::googlesql::ResolvedSetOperationScan> MakeTestSetOperationScan(
     304              :     ::googlesql::ResolvedSetOperationScan::SetOperationType op_type,
     305              :     ::googlesql::ResolvedSetOperationScan::SetOperationColumnMatchMode
     306            2 :         match_mode) {
     307            2 :   std::vector<std::unique_ptr<const ::googlesql::ResolvedSetOperationItem>>
     308            2 :       items;
     309            2 :   items.push_back(::googlesql::MakeResolvedSetOperationItem(
     310            2 :       ::googlesql::MakeResolvedSingleRowScan(),
     311            2 :       /*output_column_list=*/{}));
     312            2 :   items.push_back(::googlesql::MakeResolvedSetOperationItem(
     313            2 :       ::googlesql::MakeResolvedSingleRowScan(),
     314            2 :       /*output_column_list=*/{}));
     315            2 :   auto scan = ::googlesql::MakeResolvedSetOperationScan(
     316            2 :       /*column_list=*/{}, op_type, std::move(items));
     317            2 :   scan->set_column_match_mode(match_mode);
     318            2 :   scan->set_column_propagation_mode(
     319            2 :       ::googlesql::ResolvedSetOperationScan::STRICT);
     320            2 :   return scan;
     321            2 : }
     322              : 
     323            1 : TEST_F(TranspilerTest, EmitSetOperationScanCorrespondingEmitsKeyword) {
     324              :   // `CORRESPONDING` uses the analyzer's per-item `output_column_list`
     325              :   // mapping; `EmitSetOperationItem` projects each arm before the keyword.
     326            1 :   const ::googlesql::ResolvedStatement* stmt = Analyze(
     327            1 :       "SELECT x, y FROM (SELECT 1 AS x, 'a' AS y UNION ALL CORRESPONDING "
     328            1 :       "SELECT 'b' AS y, 2 AS x)");
     329            1 :   ASSERT_NE(stmt, nullptr);
     330            1 :   const ::googlesql::ResolvedScan* scan = QueryInputScan(stmt);
     331            1 :   ASSERT_NE(scan, nullptr);
     332            1 :   ASSERT_EQ(scan->node_kind(), ::googlesql::RESOLVED_SET_OPERATION_SCAN);
     333            1 :   const auto* set_op = scan->GetAs<::googlesql::ResolvedSetOperationScan>();
     334            1 :   ASSERT_EQ(set_op->column_match_mode(),
     335            1 :             ::googlesql::ResolvedSetOperationScan::CORRESPONDING);
     336            1 :   TestTranspiler t;
     337            1 :   const std::string sql = t.EmitSetOperationScan(set_op);
     338            1 :   EXPECT_FALSE(sql.empty());
     339            1 :   EXPECT_NE(sql.find(" UNION ALL "), std::string::npos);
     340            1 :   EXPECT_NE(sql.find("\"x\""), std::string::npos);
     341            1 :   EXPECT_NE(sql.find("\"y\""), std::string::npos);
     342            1 : }
     343              : 
     344            1 : TEST_F(TranspilerTest, EmitSetOperationScanIntersectAllEmitsKeyword) {
     345              :   // `INTERSECT_ALL` (DuckDB-native extension; not in BQ surface
     346              :   // SQL) emits the matching `INTERSECT ALL` keyword. The standard
     347              :   // SQL bag semantics (`min(m, n)`) match the GoogleSQL
     348              :   // `INTERSECT_ALL` contract per `resolved_ast.h`, so the lowered
     349              :   // SQL preserves the analyzer's intent.
     350            1 :   auto scan = MakeTestSetOperationScan(
     351            1 :       ::googlesql::ResolvedSetOperationScan::INTERSECT_ALL,
     352            1 :       ::googlesql::ResolvedSetOperationScan::BY_POSITION);
     353            1 :   TestTranspiler t;
     354            1 :   EXPECT_EQ(t.EmitSetOperationScan(scan.get()),
     355            1 :             "SELECT * FROM (SELECT 1) INTERSECT ALL SELECT * FROM (SELECT 1)");
     356            1 : }
     357              : 
     358            1 : TEST_F(TranspilerTest, EmitSetOperationScanExceptAllEmitsKeyword) {
     359              :   // `EXCEPT_ALL` similarly emits `EXCEPT ALL`. DuckDB has shipped
     360              :   // `EXCEPT ALL` with standard SQL bag-difference semantics
     361              :   // (`max(m - n, 0)`) since v0.10; the GoogleSQL contract is the
     362              :   // same.
     363            1 :   auto scan = MakeTestSetOperationScan(
     364            1 :       ::googlesql::ResolvedSetOperationScan::EXCEPT_ALL,
     365            1 :       ::googlesql::ResolvedSetOperationScan::BY_POSITION);
     366            1 :   TestTranspiler t;
     367            1 :   EXPECT_EQ(t.EmitSetOperationScan(scan.get()),
     368            1 :             "SELECT * FROM (SELECT * FROM (SELECT 1) EXCEPT ALL SELECT * "
     369            1 :             "FROM (SELECT 1)) ORDER BY 1");
     370            1 : }
     371              : 
     372              : // --- Sample scan -------------------------------------------------------
     373              : 
     374              : }  // namespace transpiler
     375              : }  // namespace duckdb
     376              : }  // namespace engine
     377              : }  // namespace backend
     378              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1