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

            Line data    Source code
       1              : // Unit tests for `SemanticExecutor`.
       2              : //
       3              : // We drive a real `AnalyzeStatement` against a tiny `SimpleCatalog`
       4              : // (mirroring the conformance harness used in
       5              : // `route_classifier_test.cc` / `stub_executors_test.cc`) and run
       6              : // `SemanticExecutor::ExecuteQuery` over the analyzer's resolved
       7              : // statement directly. The tests pin the end-to-end happy paths
       8              : // (scalar SELECT + arithmetic + parameter binding) and the
       9              : // error-surface mappings the gateway depends on
      10              : // (`SELECT 1 / 0 -> divisionByZero`, `SELECT INT64_MAX + 1 ->
      11              : // overflow`). Analytic numbering/navigation tests live in
      12              : // `executor_analytic_test.cc`.
      13              : 
      14              : #include <string>
      15              : #include <vector>
      16              : 
      17              : #include "absl/status/status.h"
      18              : #include "backend/engine/semantic/error.h"
      19              : #include "backend/engine/semantic/executor_test_fixture.h"
      20              : #include "backend/storage/storage.h"
      21              : #include "googlesql/public/analyzer_options.h"
      22              : #include "googlesql/public/id_string.h"
      23              : #include "googlesql/public/value.h"
      24              : #include "googlesql/resolved_ast/resolved_column.h"
      25              : #include "gtest/gtest.h"
      26              : 
      27              : namespace bigquery_emulator {
      28              : namespace backend {
      29              : namespace engine {
      30              : namespace semantic {
      31              : namespace {
      32              : 
      33            1 : TEST_F(SemanticExecutorTest, ScalarSelectOneRoundTrips) {
      34            1 :   auto cell = RunForFirstCell("SELECT 1");
      35            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
      36            1 :   EXPECT_EQ(cell->int64_value(), 1);
      37            1 : }
      38              : 
      39            1 : TEST_F(SemanticExecutorTest, ScalarSelectArithmeticRoundTrips) {
      40            1 :   auto cell = RunForFirstCell("SELECT 1 + 2");
      41            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
      42            1 :   EXPECT_EQ(cell->int64_value(), 3);
      43            1 : }
      44              : 
      45            1 : TEST_F(SemanticExecutorTest, ScalarSelectMultipleColumnsRoundTrip) {
      46            1 :   const auto* stmt = Analyze("SELECT 1 AS a, 'x' AS b", MakeAnalyzerOptions());
      47            1 :   ASSERT_NE(stmt, nullptr);
      48            1 :   SemanticExecutor exec;
      49            1 :   auto source =
      50            1 :       exec.ExecuteQuery(MakeRequest("SELECT 1, 'x'"), *stmt, catalog_.get());
      51            2 :   ASSERT_TRUE(source.ok()) << source.status();
      52            1 :   ASSERT_EQ((*source)->schema().columns.size(), 2u);
      53            1 :   EXPECT_EQ((*source)->schema().columns[0].name, "a");
      54            1 :   EXPECT_EQ((*source)->schema().columns[1].name, "b");
      55            1 :   storage::Row row;
      56            1 :   auto has = (*source)->Next(&row);
      57            2 :   ASSERT_TRUE(has.ok()) << has.status();
      58            1 :   ASSERT_TRUE(*has);
      59            1 :   ASSERT_EQ(row.cells.size(), 2u);
      60            1 :   EXPECT_EQ(row.cells[0].int64_value(), 1);
      61            1 :   EXPECT_EQ(row.cells[1].string_value(), "x");
      62            1 :   has = (*source)->Next(&row);
      63            2 :   ASSERT_TRUE(has.ok()) << has.status();
      64            1 :   EXPECT_FALSE(*has);
      65            1 : }
      66              : 
      67            1 : TEST_F(SemanticExecutorTest, NullAdditionPropagatesNull) {
      68            1 :   auto cell = RunForFirstCell("SELECT CAST(NULL AS INT64) + 1");
      69            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
      70            1 :   EXPECT_TRUE(cell->is_null());
      71            1 : }
      72              : 
      73            1 : TEST_F(SemanticExecutorTest, DivisionByZeroSurfacesReason) {
      74            1 :   const auto* stmt = Analyze("SELECT 1.0 / 0", MakeAnalyzerOptions());
      75            1 :   ASSERT_NE(stmt, nullptr);
      76            1 :   SemanticExecutor exec;
      77            1 :   auto source =
      78            1 :       exec.ExecuteQuery(MakeRequest("SELECT 1.0 / 0"), *stmt, catalog_.get());
      79            1 :   ASSERT_FALSE(source.ok());
      80            1 :   EXPECT_EQ(source.status().code(), absl::StatusCode::kInvalidArgument);
      81            1 :   EXPECT_EQ(GetSemanticErrorReason(source.status()),
      82            1 :             SemanticErrorReason::kDivisionByZero);
      83            1 : }
      84              : 
      85            1 : TEST_F(SemanticExecutorTest, Int64OverflowSurfacesReason) {
      86            1 :   const auto* stmt =
      87            1 :       Analyze("SELECT 9223372036854775807 + 1", MakeAnalyzerOptions());
      88            1 :   ASSERT_NE(stmt, nullptr);
      89            1 :   SemanticExecutor exec;
      90            1 :   auto source = exec.ExecuteQuery(
      91            1 :       MakeRequest("SELECT 9223372036854775807 + 1"), *stmt, catalog_.get());
      92            1 :   ASSERT_FALSE(source.ok());
      93            1 :   EXPECT_EQ(GetSemanticErrorReason(source.status()),
      94            1 :             SemanticErrorReason::kOverflow);
      95            1 : }
      96              : 
      97            1 : TEST_F(SemanticExecutorTest, SafeAddOverflowProducesNull) {
      98            1 :   auto cell = RunForFirstCell("SELECT SAFE_ADD(9223372036854775807, 1)");
      99            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
     100            1 :   EXPECT_TRUE(cell->is_null());
     101            1 : }
     102              : 
     103            1 : TEST_F(SemanticExecutorTest, NamedParameterBindsAndArithmeticUses) {
     104            1 :   ::googlesql::AnalyzerOptions options = MakeAnalyzerOptions();
     105            1 :   ASSERT_TRUE(
     106            1 :       options.AddQueryParameter("p", ::googlesql::types::Int64Type()).ok());
     107              : 
     108            1 :   const auto* stmt = Analyze("SELECT @p + 1", options);
     109            1 :   ASSERT_NE(stmt, nullptr);
     110            1 :   QueryRequest req = MakeRequest("SELECT @p + 1");
     111            1 :   QueryParameter p;
     112            1 :   p.name = "p";
     113            1 :   p.type_kind = "INT64";
     114            1 :   p.value_json = "40";
     115            1 :   req.parameters.push_back(p);
     116              : 
     117            1 :   SemanticExecutor exec;
     118            1 :   auto source = exec.ExecuteQuery(req, *stmt, catalog_.get());
     119            2 :   ASSERT_TRUE(source.ok()) << source.status();
     120            1 :   storage::Row row;
     121            1 :   auto has = (*source)->Next(&row);
     122            2 :   ASSERT_TRUE(has.ok()) << has.status();
     123            1 :   ASSERT_TRUE(*has);
     124            1 :   EXPECT_EQ(row.cells[0].int64_value(), 41);
     125            1 : }
     126              : 
     127            1 : TEST_F(SemanticExecutorTest, RejectsSelectWithFromShape) {
     128              :   // Add a fake table to the catalog so the analyzer can resolve
     129              :   // the FROM clause; the executor should still reject the shape.
     130            1 :   ::googlesql::SimpleTable* table =
     131            1 :       new ::googlesql::SimpleTable("t", {{"x", type_factory_->get_int64()}});
     132            1 :   catalog_->AddOwnedTable(table);
     133            1 :   const auto* stmt = Analyze("SELECT x FROM t", MakeAnalyzerOptions());
     134            1 :   ASSERT_NE(stmt, nullptr);
     135            1 :   SemanticExecutor exec;
     136            1 :   auto source =
     137            1 :       exec.ExecuteQuery(MakeRequest("SELECT x FROM t"), *stmt, catalog_.get());
     138            1 :   ASSERT_FALSE(source.ok());
     139            1 :   EXPECT_EQ(source.status().code(), absl::StatusCode::kUnimplemented);
     140            1 : }
     141              : 
     142              : // `docs/ENGINE_POLICY.md` Family 2. A
     143              : // `ResolvedBarrierScan` wrapping a SingleRowScan is the
     144              : // pipe-operator analog of `SELECT 1 + 2`; the barrier is the
     145              : // analyzer's pipe-boundary marker and rows pass through
     146              : // unchanged. `StripBarrierScans` peels the wrapper before
     147              : // dispatch so the scalar-only evaluator handles the projection.
     148            1 : TEST_F(SemanticExecutorTest, BarrierScanOverSingleRowPassesThrough) {
     149              :   // Direct construction: the surface SQL `SELECT 1 + 2 |> SELECT ...`
     150              :   // is not yet enabled in this fixture's analyzer, but the
     151              :   // `ResolvedQueryStmt(query=ResolvedProjectScan(input_scan=
     152              :   // ResolvedBarrierScan(input_scan=ResolvedSingleRowScan)))`
     153              :   // shape is what the analyzer would emit, so we build it
     154              :   // directly and feed it to the executor.
     155            1 :   auto single = ::googlesql::MakeResolvedSingleRowScan();
     156            1 :   auto barrier = ::googlesql::MakeResolvedBarrierScan(
     157            1 :       /*column_list=*/{}, std::move(single));
     158              :   // Project a literal 7 onto a fresh output column.
     159            1 :   ::googlesql::ResolvedColumn out_col(
     160            1 :       /*column_id=*/100,
     161            1 :       /*table_name=*/::googlesql::IdString::MakeGlobal("$query"),
     162            1 :       /*name=*/::googlesql::IdString::MakeGlobal("c"),
     163            1 :       type_factory_->get_int64());
     164            1 :   std::vector<std::unique_ptr<const ::googlesql::ResolvedComputedColumn>> exprs;
     165            1 :   exprs.push_back(::googlesql::MakeResolvedComputedColumn(
     166            1 :       out_col, ::googlesql::MakeResolvedLiteral(::googlesql::Value::Int64(7))));
     167            1 :   auto project = ::googlesql::MakeResolvedProjectScan(
     168            1 :       /*column_list=*/{out_col}, std::move(exprs), std::move(barrier));
     169            1 :   std::vector<std::unique_ptr<const ::googlesql::ResolvedOutputColumn>> outputs;
     170            1 :   outputs.push_back(
     171            1 :       ::googlesql::MakeResolvedOutputColumn(/*name=*/"c", out_col));
     172            1 :   auto query_stmt = ::googlesql::MakeResolvedQueryStmt(
     173            1 :       std::move(outputs), /*is_value_table=*/false, std::move(project));
     174              : 
     175            1 :   SemanticExecutor exec;
     176            1 :   QueryRequest req = MakeRequest("/* barrier shape; built directly */");
     177            1 :   auto source = exec.ExecuteQuery(req, *query_stmt, catalog_.get());
     178            2 :   ASSERT_TRUE(source.ok()) << source.status();
     179            1 :   storage::Row row;
     180            1 :   auto has = (*source)->Next(&row);
     181            2 :   ASSERT_TRUE(has.ok()) << has.status();
     182            1 :   ASSERT_TRUE(*has);
     183            1 :   ASSERT_EQ(row.cells.size(), 1u);
     184            1 :   EXPECT_EQ(row.cells[0].int64_value(), 7);
     185            1 : }
     186              : 
     187            1 : TEST_F(SemanticExecutorTest, UnnestWithOffsetEmitsRowPerElement) {
     188              :   // deferred work tracked in docs/ENGINE_POLICY.md: a
     189              :   // standalone `UNNEST(...) WITH OFFSET` flowing through the
     190              :   // semantic executor produces one row per element with two
     191              :   // columns (the element value + the 0-based offset).
     192            1 :   const std::string sql =
     193            1 :       "SELECT n, idx FROM UNNEST(['a', 'b', 'c']) AS n WITH OFFSET AS idx";
     194            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     195            1 :   ASSERT_NE(stmt, nullptr);
     196            1 :   SemanticExecutor exec;
     197            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     198            2 :   ASSERT_TRUE(source.ok()) << source.status();
     199            1 :   ASSERT_EQ((*source)->schema().columns.size(), 2u);
     200            1 :   EXPECT_EQ((*source)->schema().columns[0].name, "n");
     201            1 :   EXPECT_EQ((*source)->schema().columns[1].name, "idx");
     202              : 
     203            1 :   storage::Row row;
     204            4 :   for (int i = 0; i < 3; ++i) {
     205            3 :     auto has = (*source)->Next(&row);
     206            6 :     ASSERT_TRUE(has.ok()) << has.status();
     207            6 :     ASSERT_TRUE(*has) << "expected row #" << i;
     208            3 :     ASSERT_EQ(row.cells.size(), 2u);
     209            3 :     EXPECT_EQ(row.cells[1].int64_value(), i);
     210            3 :   }
     211              :   // Confirm the stream ends after 3 elements.
     212            1 :   auto has = (*source)->Next(&row);
     213            2 :   ASSERT_TRUE(has.ok()) << has.status();
     214            1 :   EXPECT_FALSE(*has);
     215            1 : }
     216              : 
     217            1 : TEST_F(SemanticExecutorTest, OuterUnnestEmptyArrayEmitsNullRow) {
     218              :   // Family 2: an empty array under `is_outer=true` (the analyzer
     219              :   // synthesizes this for the `LEFT JOIN UNNEST(...) ON TRUE`
     220              :   // pattern, and for `WITH OFFSET` against an empty literal) emits
     221              :   // a single row whose element + offset are both NULL.
     222            1 :   const std::string sql =
     223            1 :       "SELECT n, idx FROM UNNEST(CAST([] AS ARRAY<INT64>)) AS n "
     224            1 :       "WITH OFFSET AS idx";
     225            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     226            1 :   if (stmt == nullptr) {
     227            0 :     GTEST_SKIP() << "analyzer rejected empty-array literal; "
     228            0 :                     "covered by array_scan_test.";
     229            0 :   }
     230            1 :   SemanticExecutor exec;
     231            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     232            2 :   ASSERT_TRUE(source.ok()) << source.status();
     233            1 :   storage::Row row;
     234            1 :   auto has = (*source)->Next(&row);
     235            2 :   ASSERT_TRUE(has.ok()) << has.status();
     236              :   // Inner UNNEST against empty array emits zero rows; outer would
     237              :   // emit one NULL row. `WITH OFFSET` without `is_outer` is inner.
     238            1 :   EXPECT_FALSE(*has);
     239            1 : }
     240              : 
     241            1 : TEST_F(SemanticExecutorTest, DmlSurfacesNotImplemented) {
     242            1 :   const auto* stmt = Analyze("SELECT 1", MakeAnalyzerOptions());
     243            1 :   ASSERT_NE(stmt, nullptr);
     244            1 :   SemanticExecutor exec;
     245            1 :   auto out = exec.ExecuteDml(MakeRequest("SELECT 1"), *stmt, catalog_.get());
     246            1 :   ASSERT_FALSE(out.ok());
     247            1 :   EXPECT_EQ(out.status().code(), absl::StatusCode::kUnimplemented);
     248            1 : }
     249              : 
     250            1 : TEST_F(SemanticExecutorTest, DdlSurfacesNotImplemented) {
     251            1 :   const auto* stmt = Analyze("SELECT 1", MakeAnalyzerOptions());
     252            1 :   ASSERT_NE(stmt, nullptr);
     253            1 :   SemanticExecutor exec;
     254            1 :   absl::Status out =
     255            1 :       exec.ExecuteDdl(MakeRequest("SELECT 1"), *stmt, catalog_.get());
     256            1 :   ASSERT_FALSE(out.ok());
     257            1 :   EXPECT_EQ(out.code(), absl::StatusCode::kUnimplemented);
     258            1 : }
     259              : 
     260              : // R16 (conformance/REGRESSIONS.md): MIN/MAX over non-numeric orderable
     261              : // argument types (DATE, STRING, ...) used to fall through the aggregate
     262              : // dispatch as kNotImplemented and surface as the generic
     263              : // "semantic: aggregate 'max' is not implemented".
     264            1 : TEST_F(SemanticExecutorTest, MaxOverDateReturnsLatestDate) {
     265            1 :   const std::string sql =
     266            1 :       "WITH orders AS ("
     267            1 :       "  SELECT DATE '2024-01-10' AS order_date UNION ALL"
     268            1 :       "  SELECT DATE '2024-03-05' UNION ALL"
     269            1 :       "  SELECT DATE '2024-02-20') "
     270            1 :       "SELECT MAX(order_date) FROM orders";
     271            1 :   auto cell = RunForFirstCell(sql);
     272            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
     273            1 :   EXPECT_EQ(cell->string_value(), "2024-03-05");
     274            1 : }
     275              : 
     276            1 : TEST_F(SemanticExecutorTest, MinOverStringReturnsSmallestByCodePoint) {
     277            1 :   const std::string sql =
     278            1 :       "WITH names AS ("
     279            1 :       "  SELECT 'pear' AS n UNION ALL SELECT 'apple' UNION ALL SELECT 'plum') "
     280            1 :       "SELECT MIN(n) FROM names";
     281            1 :   auto cell = RunForFirstCell(sql);
     282            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
     283            1 :   EXPECT_EQ(cell->string_value(), "apple");
     284            1 : }
     285              : 
     286            1 : TEST_F(SemanticExecutorTest, MaxOverAllNullDatesReturnsNull) {
     287            1 :   const std::string sql =
     288            1 :       "WITH d AS (SELECT CAST(NULL AS DATE) AS x UNION ALL"
     289            1 :       "           SELECT CAST(NULL AS DATE)) "
     290            1 :       "SELECT MAX(x) FROM d";
     291            1 :   auto cell = RunForFirstCell(sql);
     292            2 :   ASSERT_TRUE(cell.ok()) << cell.status();
     293            1 :   EXPECT_TRUE(cell->is_null());
     294            1 : }
     295              : 
     296            1 : TEST_F(SemanticExecutorTest, ChainedCteReferencesPriorEntry) {
     297            1 :   const std::string sql =
     298            1 :       "WITH base AS (SELECT 1 AS n UNION ALL SELECT 2 AS n), "
     299            1 :       "     doubled AS (SELECT n * 2 AS m FROM base) "
     300            1 :       "SELECT SUM(m) AS total FROM doubled";
     301            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     302            1 :   ASSERT_NE(stmt, nullptr);
     303            1 :   SemanticExecutor exec;
     304            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     305            2 :   ASSERT_TRUE(source.ok()) << source.status();
     306            1 :   storage::Row row;
     307            1 :   auto has = (*source)->Next(&row);
     308            2 :   ASSERT_TRUE(has.ok()) << has.status();
     309            1 :   ASSERT_TRUE(*has);
     310            1 :   ASSERT_EQ(row.cells.size(), 1u);
     311            1 :   EXPECT_EQ(row.cells[0].int64_value(), 6);
     312            1 : }
     313              : 
     314            1 : TEST_F(SemanticExecutorTest, ChainedCteWithRowNumberAnalyticScan) {
     315            1 :   const std::string sql =
     316            1 :       "WITH base AS (SELECT 1 AS id UNION ALL SELECT 1 AS id), "
     317            1 :       "     ranked AS ("
     318            1 :       "       SELECT id, ROW_NUMBER() OVER (PARTITION BY id ORDER BY id) AS rn "
     319            1 :       "       FROM base"
     320            1 :       "     ) "
     321            1 :       "SELECT COUNT(*) AS c FROM ranked WHERE rn = 1";
     322            1 :   const auto* stmt = Analyze(sql, MakeAnalyzerOptions());
     323            1 :   ASSERT_NE(stmt, nullptr);
     324            1 :   SemanticExecutor exec;
     325            1 :   auto source = exec.ExecuteQuery(MakeRequest(sql), *stmt, catalog_.get());
     326            2 :   ASSERT_TRUE(source.ok()) << source.status();
     327            1 :   storage::Row row;
     328            1 :   auto has = (*source)->Next(&row);
     329            2 :   ASSERT_TRUE(has.ok()) << has.status();
     330            1 :   ASSERT_TRUE(*has);
     331            1 :   ASSERT_EQ(row.cells.size(), 1u);
     332            1 :   EXPECT_EQ(row.cells[0].int64_value(), 1);
     333            1 : }
     334              : 
     335              : }  // namespace
     336              : }  // namespace semantic
     337              : }  // namespace engine
     338              : }  // namespace backend
     339              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1