LCOV - code coverage report
Current view: top level - engine/semantic - executor_analytic_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 310 310
Test Date: 2026-08-08 08:38:15 Functions: 100.0 % 9 9

            Line data    Source code
       1              : // Analytic numbering / navigation / aggregate-window tests for
       2              : // `SemanticExecutor` (R14 NTILE, R15 RANK/DENSE_RANK/LAG/LEAD,
       3              : // frame-aware SUM/AVG/MIN/MAX/COUNT). Fixture shared via
       4              : // `executor_test_fixture.h`.
       5              : 
       6              : #include <cstdint>
       7              : #include <string>
       8              : #include <utility>
       9              : #include <vector>
      10              : 
      11              : #include "absl/status/status.h"
      12              : #include "backend/engine/semantic/executor.h"
      13              : #include "backend/engine/semantic/executor_test_fixture.h"
      14              : #include "backend/storage/storage.h"
      15              : #include "googlesql/public/analyzer.h"
      16              : #include "googlesql/public/analyzer_options.h"
      17              : #include "gtest/gtest.h"
      18              : 
      19              : namespace bigquery_emulator {
      20              : namespace backend {
      21              : namespace engine {
      22              : namespace semantic {
      23              : namespace {
      24              : 
      25              : // R14: NTILE over aggregate input (RFM shape) — semantic executor path.
      26            1 : TEST_F(SemanticExecutorTest, NtileOverAggregateInputUnevenBuckets) {
      27            1 :   const std::string sql =
      28            1 :       "WITH rfm_raw AS ("
      29            1 :       "  SELECT customer_id, SUM(amount) AS monetary FROM ("
      30            1 :       "    SELECT 1 AS customer_id, 100 AS amount UNION ALL"
      31            1 :       "    SELECT 2, 200 UNION ALL SELECT 3, 300 UNION ALL"
      32            1 :       "    SELECT 4, 400 UNION ALL SELECT 5, 500 UNION ALL"
      33            1 :       "    SELECT 6, 600 UNION ALL SELECT 7, 700"
      34            1 :       "  ) GROUP BY customer_id"
      35            1 :       "), rfm_scored AS ("
      36            1 :       "  SELECT customer_id, monetary,"
      37            1 :       "         NTILE(5) OVER (ORDER BY monetary ASC) AS m_score"
      38            1 :       "  FROM rfm_raw"
      39            1 :       ") "
      40            1 :       "SELECT customer_id, m_score FROM rfm_scored ORDER BY customer_id";
      41            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
      42            1 :   ASSERT_NE(stmt, nullptr);
      43            1 :   SemanticExecutor exec;
      44            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
      45            2 :   ASSERT_TRUE(source.ok()) << source.status();
      46              :   // 7 rows into 5 buckets → sizes 2,2,1,1,1 for customers 1..7.
      47            1 :   const std::vector<int64_t> want_scores = {1, 1, 2, 2, 3, 4, 5};
      48            8 :   for (size_t i = 0; i < want_scores.size(); ++i) {
      49            7 :     storage::Row row;
      50            7 :     auto has = (*source)->Next(&row);
      51           14 :     ASSERT_TRUE(has.ok()) << has.status();
      52           14 :     ASSERT_TRUE(*has) << "missing row " << i;
      53            7 :     ASSERT_EQ(row.cells.size(), 2u);
      54            7 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
      55           14 :     EXPECT_EQ(row.cells[1].int64_value(), want_scores[i]) << "row " << i;
      56            7 :   }
      57            1 :   storage::Row extra;
      58            1 :   auto has_extra = (*source)->Next(&extra);
      59            1 :   ASSERT_TRUE(has_extra.ok());
      60            1 :   EXPECT_FALSE(*has_extra);
      61            1 : }
      62              : 
      63            1 : TEST_F(SemanticExecutorTest, NtileNonPositiveBucketsRejectedAtAnalyze) {
      64              :   // GoogleSQL rejects a constant non-positive NTILE argument before
      65              :   // execution; ApplyAnalyticNtile also guards the runtime path for
      66              :   // non-constant expressions that slip through.
      67            1 :   const std::string sql =
      68            1 :       "WITH rfm_raw AS ("
      69            1 :       "  SELECT customer_id, SUM(amount) AS monetary FROM ("
      70            1 :       "    SELECT 1 AS customer_id, 100 AS amount UNION ALL SELECT 2, 200"
      71            1 :       "  ) GROUP BY customer_id"
      72            1 :       ") "
      73            1 :       "SELECT NTILE(0) OVER (ORDER BY monetary ASC) AS m_score FROM rfm_raw";
      74            1 :   last_output_.reset();
      75            1 :   absl::Status s = ::googlesql::AnalyzeStatement(sql,
      76            1 :                                                  MakeAnalyzerOptions(),
      77            1 :                                                  catalog_.get(),
      78            1 :                                                  type_factory_.get(),
      79            1 :                                                  &last_output_);
      80            1 :   EXPECT_FALSE(s.ok());
      81            2 :   EXPECT_NE(std::string(s.message()).find("NTILE"), std::string::npos) << s;
      82            1 : }
      83              : 
      84              : // R15: RANK / DENSE_RANK over nested aggregate with ties.
      85            1 : TEST_F(SemanticExecutorTest, RankDenseRankOverNestedAggregateTies) {
      86            1 :   const std::string sql =
      87            1 :       "SELECT customer_id,"
      88            1 :       "       RANK() OVER (ORDER BY SUM(amount) DESC) AS rnk,"
      89            1 :       "       DENSE_RANK() OVER (ORDER BY SUM(amount) DESC) AS drnk "
      90            1 :       "FROM ("
      91            1 :       "  SELECT 1 AS customer_id, 100 AS amount UNION ALL"
      92            1 :       "  SELECT 1, 50 UNION ALL"
      93            1 :       "  SELECT 2, 100 UNION ALL"
      94            1 :       "  SELECT 2, 50 UNION ALL"
      95            1 :       "  SELECT 3, 200 UNION ALL"
      96            1 :       "  SELECT 4, 50"
      97            1 :       ") GROUP BY customer_id "
      98            1 :       "ORDER BY customer_id";
      99            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     100            1 :   ASSERT_NE(stmt, nullptr);
     101            1 :   SemanticExecutor exec;
     102            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     103            2 :   ASSERT_TRUE(source.ok()) << source.status();
     104              :   // Sums DESC: 200,150,150,50 → RANK 1,2,2,4 and DENSE_RANK 1,2,2,3.
     105              :   // Ordered by customer_id: c1=150→2/2, c2=150→2/2, c3=200→1/1, c4=50→4/3.
     106            1 :   const std::vector<std::pair<int64_t, int64_t>> want = {
     107            1 :       {2, 2}, {2, 2}, {1, 1}, {4, 3}};
     108            5 :   for (size_t i = 0; i < want.size(); ++i) {
     109            4 :     storage::Row row;
     110            4 :     auto has = (*source)->Next(&row);
     111            8 :     ASSERT_TRUE(has.ok()) << has.status();
     112            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     113            4 :     ASSERT_EQ(row.cells.size(), 3u);
     114            4 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
     115            8 :     EXPECT_EQ(row.cells[1].int64_value(), want[i].first) << "rank row " << i;
     116            8 :     EXPECT_EQ(row.cells[2].int64_value(), want[i].second)
     117            8 :         << "dense_rank row " << i;
     118            4 :   }
     119            1 :   storage::Row extra;
     120            1 :   auto has_extra = (*source)->Next(&extra);
     121            1 :   ASSERT_TRUE(has_extra.ok());
     122            1 :   EXPECT_FALSE(*has_extra);
     123            1 : }
     124              : 
     125              : // Analytic output is stabilized on PARTITION BY + ORDER BY (no outer
     126              : // ORDER BY), matching DuckDB CaptureAnalyticOutputOrder. Pins the
     127              : // gateway e2e LEAD/NTILE finishers shape when FORMAT_TIMESTAMP promotes
     128              : // the query onto the semantic route.
     129            1 : TEST_F(SemanticExecutorTest, LeadOutputOrderFollowsPartitionAndOrderBy) {
     130            1 :   const std::string sql =
     131            1 :       "WITH finishers AS ("
     132            1 :       "  SELECT 'Sophia' AS name, TIMESTAMP '2016-10-18 02:51:45+00' AS "
     133            1 :       "finish_time, 'F30-34' AS division UNION ALL"
     134            1 :       "  SELECT 'Lisa', TIMESTAMP '2016-10-18 02:54:11+00', 'F35-39' UNION ALL"
     135            1 :       "  SELECT 'Nikki', TIMESTAMP '2016-10-18 02:59:01+00', 'F30-34' UNION ALL"
     136            1 :       "  SELECT 'Carly', TIMESTAMP '2016-10-18 03:08:58+00', 'F25-29'"
     137            1 :       ") "
     138            1 :       "SELECT name, division,"
     139            1 :       "       LEAD(name) OVER (PARTITION BY division ORDER BY finish_time ASC) "
     140            1 :       "AS followed_by "
     141            1 :       "FROM finishers";
     142            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     143            1 :   ASSERT_NE(stmt, nullptr);
     144            1 :   SemanticExecutor exec;
     145            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     146            2 :   ASSERT_TRUE(source.ok()) << source.status();
     147            1 :   struct Want {
     148            1 :     const char* name;
     149            1 :     const char* division;
     150            1 :     bool followed_null;
     151            1 :     const char* followed;
     152            1 :   };
     153            1 :   const std::vector<Want> want = {
     154            1 :       {"Carly", "F25-29", true, nullptr},
     155            1 :       {"Sophia", "F30-34", false, "Nikki"},
     156            1 :       {"Nikki", "F30-34", true, nullptr},
     157            1 :       {"Lisa", "F35-39", true, nullptr},
     158            1 :   };
     159            5 :   for (size_t i = 0; i < want.size(); ++i) {
     160            4 :     storage::Row row;
     161            4 :     auto has = (*source)->Next(&row);
     162            8 :     ASSERT_TRUE(has.ok()) << has.status();
     163            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     164            4 :     ASSERT_EQ(row.cells.size(), 3u);
     165            8 :     EXPECT_EQ(row.cells[0].string_value(), want[i].name) << "row " << i;
     166            8 :     EXPECT_EQ(row.cells[1].string_value(), want[i].division) << "row " << i;
     167            4 :     if (want[i].followed_null) {
     168            6 :       EXPECT_TRUE(row.cells[2].is_null()) << "lead row " << i;
     169            3 :     } else {
     170            2 :       ASSERT_FALSE(row.cells[2].is_null()) << "lead row " << i;
     171            2 :       EXPECT_EQ(row.cells[2].string_value(), want[i].followed) << "row " << i;
     172            1 :     }
     173            4 :   }
     174            1 :   storage::Row extra;
     175            1 :   auto has_extra = (*source)->Next(&extra);
     176            1 :   ASSERT_TRUE(has_extra.ok());
     177            1 :   EXPECT_FALSE(*has_extra);
     178            1 : }
     179              : 
     180              : // R15: LAG / LEAD over nested aggregate with offset and default.
     181            1 : TEST_F(SemanticExecutorTest, LagLeadOverNestedAggregate) {
     182            1 :   const std::string sql =
     183            1 :       "SELECT customer_id,"
     184            1 :       "       LAG(SUM(amount), 1) OVER (ORDER BY customer_id) AS prev_m,"
     185            1 :       "       LEAD(SUM(amount), 2, 0) OVER (ORDER BY customer_id) AS next2_m "
     186            1 :       "FROM ("
     187            1 :       "  SELECT 1 AS customer_id, 100 AS amount UNION ALL"
     188            1 :       "  SELECT 2, 200 UNION ALL"
     189            1 :       "  SELECT 3, 300 UNION ALL"
     190            1 :       "  SELECT 4, 400"
     191            1 :       ") GROUP BY customer_id "
     192            1 :       "ORDER BY customer_id";
     193            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     194            1 :   ASSERT_NE(stmt, nullptr);
     195            1 :   SemanticExecutor exec;
     196            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     197            2 :   ASSERT_TRUE(source.ok()) << source.status();
     198              :   // monetary: 100,200,300,400 → LAG1: NULL,100,200,300; LEAD2 default0:
     199              :   // 300,400,0,0
     200            1 :   struct Want {
     201            1 :     bool prev_null;
     202            1 :     int64_t prev;
     203            1 :     int64_t next2;
     204            1 :   };
     205            1 :   const std::vector<Want> want = {
     206            1 :       {true, 0, 300}, {false, 100, 400}, {false, 200, 0}, {false, 300, 0}};
     207            5 :   for (size_t i = 0; i < want.size(); ++i) {
     208            4 :     storage::Row row;
     209            4 :     auto has = (*source)->Next(&row);
     210            8 :     ASSERT_TRUE(has.ok()) << has.status();
     211            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     212            4 :     ASSERT_EQ(row.cells.size(), 3u);
     213            4 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
     214            4 :     if (want[i].prev_null) {
     215            2 :       EXPECT_TRUE(row.cells[1].is_null()) << "lag row " << i;
     216            3 :     } else {
     217            6 :       ASSERT_FALSE(row.cells[1].is_null()) << "lag row " << i;
     218            6 :       EXPECT_EQ(row.cells[1].int64_value(), want[i].prev) << "lag row " << i;
     219            3 :     }
     220            8 :     ASSERT_FALSE(row.cells[2].is_null()) << "lead row " << i;
     221            8 :     EXPECT_EQ(row.cells[2].int64_value(), want[i].next2) << "lead row " << i;
     222            4 :   }
     223            1 :   storage::Row extra;
     224            1 :   auto has_extra = (*source)->Next(&extra);
     225            1 :   ASSERT_TRUE(has_extra.ok());
     226            1 :   EXPECT_FALSE(*has_extra);
     227            1 : }
     228              : 
     229              : // Whole-partition MAX/MIN/AVG/COUNT(*) over aggregate input.
     230            1 : TEST_F(SemanticExecutorTest, AggregateWindowsWholePartitionOverGroups) {
     231            1 :   const std::string sql =
     232            1 :       "SELECT customer_id,"
     233            1 :       "       SUM(amount) AS monetary,"
     234            1 :       "       MAX(SUM(amount)) OVER () AS max_m,"
     235            1 :       "       MIN(SUM(amount)) OVER () AS min_m,"
     236            1 :       "       AVG(SUM(amount)) OVER () AS avg_m,"
     237            1 :       "       COUNT(*) OVER () AS n_groups "
     238            1 :       "FROM ("
     239            1 :       "  SELECT 1 AS customer_id, 100 AS amount UNION ALL"
     240            1 :       "  SELECT 2, 200 UNION ALL"
     241            1 :       "  SELECT 3, 300"
     242            1 :       ") GROUP BY customer_id "
     243            1 :       "ORDER BY customer_id";
     244            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     245            1 :   ASSERT_NE(stmt, nullptr);
     246            1 :   SemanticExecutor exec;
     247            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     248            2 :   ASSERT_TRUE(source.ok()) << source.status();
     249              :   // Group sums: 100,200,300 → max=300, min=100, avg=200, count=3.
     250            4 :   for (int64_t id = 1; id <= 3; ++id) {
     251            3 :     storage::Row row;
     252            3 :     auto has = (*source)->Next(&row);
     253            6 :     ASSERT_TRUE(has.ok()) << has.status();
     254            6 :     ASSERT_TRUE(*has) << "missing row " << id;
     255            3 :     ASSERT_EQ(row.cells.size(), 6u);
     256            3 :     EXPECT_EQ(row.cells[0].int64_value(), id);
     257            3 :     EXPECT_EQ(row.cells[1].int64_value(), id * 100);
     258            3 :     EXPECT_EQ(row.cells[2].int64_value(), 300);
     259            3 :     EXPECT_EQ(row.cells[3].int64_value(), 100);
     260            3 :     EXPECT_DOUBLE_EQ(row.cells[4].float64_value(), 200.0);
     261            3 :     EXPECT_EQ(row.cells[5].int64_value(), 3);
     262            3 :   }
     263            1 :   storage::Row extra;
     264            1 :   auto has_extra = (*source)->Next(&extra);
     265            1 :   ASSERT_TRUE(has_extra.ok());
     266            1 :   EXPECT_FALSE(*has_extra);
     267            1 : }
     268              : 
     269              : // Default RANGE frame running SUM with a peer tie.
     270            1 : TEST_F(SemanticExecutorTest, RunningSumOverNestedAggregateWithTies) {
     271            1 :   const std::string sql =
     272            1 :       "SELECT customer_id,"
     273            1 :       "       SUM(amount) AS monetary,"
     274            1 :       "       SUM(SUM(amount)) OVER (ORDER BY SUM(amount)) AS running "
     275            1 :       "FROM ("
     276            1 :       "  SELECT 1 AS customer_id, 100 AS amount UNION ALL"
     277            1 :       "  SELECT 2, 100 UNION ALL"
     278            1 :       "  SELECT 3, 200 UNION ALL"
     279            1 :       "  SELECT 4, 50"
     280            1 :       ") GROUP BY customer_id "
     281            1 :       "ORDER BY customer_id";
     282            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     283            1 :   ASSERT_NE(stmt, nullptr);
     284            1 :   SemanticExecutor exec;
     285            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     286            2 :   ASSERT_TRUE(source.ok()) << source.status();
     287              :   // Ordered by monetary ASC: 50(c4), 100(c1), 100(c2), 200(c3).
     288              :   // Default RANGE UNBOUNDED PRECEDING .. CURRENT ROW includes peers, so
     289              :   // both 100-rows see 50+100+100=250 and the 200-row sees 450.
     290              :   // Ordered by customer_id: c1→250, c2→250, c3→450, c4→50.
     291            1 :   const std::vector<int64_t> want_running = {250, 250, 450, 50};
     292            5 :   for (size_t i = 0; i < want_running.size(); ++i) {
     293            4 :     storage::Row row;
     294            4 :     auto has = (*source)->Next(&row);
     295            8 :     ASSERT_TRUE(has.ok()) << has.status();
     296            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     297            4 :     ASSERT_EQ(row.cells.size(), 3u);
     298            4 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
     299            8 :     EXPECT_EQ(row.cells[2].int64_value(), want_running[i])
     300            8 :         << "running row " << i;
     301            4 :   }
     302            1 : }
     303              : 
     304              : // Explicit ROWS frame: preceding + current only (no peer expansion).
     305            1 : TEST_F(SemanticExecutorTest, RowsFrameSumOverNestedAggregate) {
     306            1 :   const std::string sql =
     307            1 :       "SELECT customer_id,"
     308            1 :       "       SUM(SUM(amount)) OVER ("
     309            1 :       "         ORDER BY customer_id"
     310            1 :       "         ROWS BETWEEN 1 PRECEDING AND CURRENT ROW"
     311            1 :       "       ) AS win "
     312            1 :       "FROM ("
     313            1 :       "  SELECT 1 AS customer_id, 100 AS amount UNION ALL"
     314            1 :       "  SELECT 2, 200 UNION ALL"
     315            1 :       "  SELECT 3, 300 UNION ALL"
     316            1 :       "  SELECT 4, 400"
     317            1 :       ") GROUP BY customer_id "
     318            1 :       "ORDER BY customer_id";
     319            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     320            1 :   ASSERT_NE(stmt, nullptr);
     321            1 :   SemanticExecutor exec;
     322            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     323            2 :   ASSERT_TRUE(source.ok()) << source.status();
     324              :   // Row sums 100,200,300,400 → windows: 100, 300, 500, 700.
     325            1 :   const std::vector<int64_t> want = {100, 300, 500, 700};
     326            5 :   for (size_t i = 0; i < want.size(); ++i) {
     327            4 :     storage::Row row;
     328            4 :     auto has = (*source)->Next(&row);
     329            8 :     ASSERT_TRUE(has.ok()) << has.status();
     330            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     331            4 :     ASSERT_EQ(row.cells.size(), 2u);
     332            4 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
     333            8 :     EXPECT_EQ(row.cells[1].int64_value(), want[i]) << "rows frame row " << i;
     334            4 :   }
     335            1 : }
     336              : 
     337              : // Numeric RANGE value-offset COUNT (replaces the old CountRange lane;
     338              : // BigQuery requires a numeric ORDER BY for RANGE offsets).
     339            1 : TEST_F(SemanticExecutorTest, NumericRangeCountOverAggregateInput) {
     340            1 :   const std::string sql =
     341            1 :       "SELECT customer_id,"
     342            1 :       "       COUNT(*) OVER ("
     343            1 :       "         ORDER BY SUM(amount)"
     344            1 :       "         RANGE BETWEEN 1 PRECEDING AND CURRENT ROW"
     345            1 :       "       ) AS cnt "
     346            1 :       "FROM ("
     347            1 :       "  SELECT 1 AS customer_id, 10 AS amount UNION ALL"
     348            1 :       "  SELECT 2, 11 UNION ALL"
     349            1 :       "  SELECT 3, 12 UNION ALL"
     350            1 :       "  SELECT 4, 20"
     351            1 :       ") GROUP BY customer_id "
     352            1 :       "ORDER BY customer_id";
     353            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     354            1 :   ASSERT_NE(stmt, nullptr);
     355            1 :   SemanticExecutor exec;
     356            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     357            2 :   ASSERT_TRUE(source.ok()) << source.status();
     358              :   // Sums 10,11,12,20 → RANGE ±1 preceding..current:
     359              :   // 10→{10}, 11→{10,11}, 12→{11,12}, 20→{20}.
     360            1 :   const std::vector<int64_t> want = {1, 2, 2, 1};
     361            5 :   for (size_t i = 0; i < want.size(); ++i) {
     362            4 :     storage::Row row;
     363            4 :     auto has = (*source)->Next(&row);
     364            8 :     ASSERT_TRUE(has.ok()) << has.status();
     365            8 :     ASSERT_TRUE(*has) << "missing row " << i;
     366            4 :     ASSERT_EQ(row.cells.size(), 2u);
     367            4 :     EXPECT_EQ(row.cells[0].int64_value(), static_cast<int64_t>(i + 1));
     368            8 :     EXPECT_EQ(row.cells[1].int64_value(), want[i]) << "range count row " << i;
     369            4 :   }
     370            1 : }
     371              : 
     372              : }  // namespace
     373              : }  // namespace semantic
     374              : }  // namespace engine
     375              : }  // namespace backend
     376              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1