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
|