Line data Source code
1 : // Unit tests for the local DML executor.
2 : //
3 : // We drive the analyzer against a small in-memory `FakeStorage` +
4 : // `SimpleCatalog` populated with a `StorageTable` so the resolved
5 : // AST the executor sees is the same shape the production
6 : // coordinator produces. Storage round-trip is verified by reading
7 : // back through `FakeStorage`.
8 :
9 : #include "backend/engine/semantic/dml/dml_executor.h"
10 :
11 : #include <cstddef>
12 : #include <cstdint>
13 : #include <map>
14 : #include <memory>
15 : #include <string>
16 : #include <utility>
17 : #include <vector>
18 :
19 : #include "absl/status/status.h"
20 : #include "absl/status/statusor.h"
21 : #include "absl/types/span.h"
22 : #include "backend/catalog/storage_table.h"
23 : #include "backend/engine/engine.h"
24 : #include "backend/engine/semantic/error.h"
25 : #include "backend/schema/schema.h"
26 : #include "backend/storage/storage.h"
27 : #include "googlesql/public/analyzer.h"
28 : #include "googlesql/public/analyzer_options.h"
29 : #include "googlesql/public/analyzer_output.h"
30 : #include "googlesql/public/builtin_function_options.h"
31 : #include "googlesql/public/catalog.h"
32 : #include "googlesql/public/language_options.h"
33 : #include "googlesql/public/options.pb.h"
34 : #include "googlesql/public/simple_catalog.h"
35 : #include "googlesql/public/type.h"
36 : #include "googlesql/public/type.pb.h"
37 : #include "googlesql/public/types/type_factory.h"
38 : #include "googlesql/resolved_ast/resolved_ast.h"
39 : #include "gtest/gtest.h"
40 :
41 : namespace bigquery_emulator {
42 : namespace backend {
43 : namespace engine {
44 : namespace semantic {
45 : namespace dml {
46 : namespace {
47 :
48 : // Minimal in-memory `Storage` impl. The DML executor only needs
49 : // `GetSchema`, `AppendRows`, `OverwriteRows`, and `ScanRows`; the
50 : // dataset/table CRUD, list, and Storage Read API methods stay
51 : // `kUnimplemented` so the test stays focused.
52 : class FakeStorage : public storage::Storage {
53 : public:
54 : // Pre-register a table at `id` with `schema`. Subsequent
55 : // append/overwrite/scan calls operate against the buffer keyed
56 : // by the canonical "<project>/<dataset>/<table>" string.
57 10 : void RegisterTable(const storage::TableId& id, schema::TableSchema schema) {
58 10 : schemas_[Key(id)] = std::move(schema);
59 10 : rows_[Key(id)] = {};
60 10 : }
61 :
62 9 : const std::vector<storage::Row>& Rows(const storage::TableId& id) const {
63 9 : return rows_.at(Key(id));
64 9 : }
65 :
66 : // ---- Storage interface ----
67 : absl::Status CreateDataset(const storage::DatasetId& /*id*/,
68 0 : absl::string_view /*location*/) override {
69 0 : return absl::UnimplementedError("FakeStorage::CreateDataset");
70 0 : }
71 : absl::Status DropDataset(const storage::DatasetId& /*id*/,
72 : bool /*delete_contents*/,
73 0 : absl::string_view = {}) override {
74 0 : return absl::UnimplementedError("FakeStorage::DropDataset");
75 0 : }
76 : absl::Status CreateTable(const storage::TableId& /*id*/,
77 0 : const schema::TableSchema& /*schema*/) override {
78 0 : return absl::UnimplementedError("FakeStorage::CreateTable");
79 0 : }
80 0 : absl::Status DropTable(const storage::TableId& /*id*/) override {
81 0 : return absl::UnimplementedError("FakeStorage::DropTable");
82 0 : }
83 : absl::StatusOr<std::vector<storage::DatasetId>> ListDatasets(
84 0 : absl::string_view /*project_id*/) const override {
85 0 : return absl::UnimplementedError("FakeStorage::ListDatasets");
86 0 : }
87 : absl::StatusOr<std::vector<storage::TableId>> ListTables(
88 0 : const storage::DatasetId& /*dataset_id*/) const override {
89 0 : return absl::UnimplementedError("FakeStorage::ListTables");
90 0 : }
91 : absl::StatusOr<schema::TableSchema> GetSchema(
92 0 : const storage::TableId& id) const override {
93 0 : auto it = schemas_.find(Key(id));
94 0 : if (it == schemas_.end()) {
95 0 : return absl::NotFoundError(
96 0 : "FakeStorage::GetSchema: table not registered");
97 0 : }
98 0 : return it->second;
99 0 : }
100 : absl::Status AppendRows(const storage::TableId& id,
101 8 : absl::Span<const storage::Row> rows) override {
102 8 : auto it = rows_.find(Key(id));
103 8 : if (it == rows_.end()) {
104 0 : return absl::NotFoundError(
105 0 : "FakeStorage::AppendRows: table not registered");
106 0 : }
107 8 : for (const storage::Row& r : rows)
108 15 : it->second.push_back(r);
109 8 : return absl::OkStatus();
110 8 : }
111 : absl::Status OverwriteRows(const storage::TableId& id,
112 5 : absl::Span<const storage::Row> rows) override {
113 5 : auto it = rows_.find(Key(id));
114 5 : if (it == rows_.end()) {
115 0 : return absl::NotFoundError(
116 0 : "FakeStorage::OverwriteRows: table not registered");
117 0 : }
118 5 : it->second.assign(rows.begin(), rows.end());
119 5 : return absl::OkStatus();
120 5 : }
121 :
122 : class VectorIterator : public storage::RowIterator {
123 : public:
124 : explicit VectorIterator(std::vector<storage::Row> rows)
125 5 : : rows_(std::move(rows)) {}
126 15 : absl::StatusOr<bool> Next(storage::Row* row) override {
127 15 : if (cursor_ >= rows_.size()) return false;
128 10 : *row = rows_[cursor_++];
129 10 : return true;
130 15 : }
131 :
132 : private:
133 : std::vector<storage::Row> rows_{};
134 : size_t cursor_ = 0;
135 : };
136 :
137 : absl::StatusOr<std::unique_ptr<storage::RowIterator>> ScanRows(
138 5 : const storage::TableId& id) const override {
139 5 : auto it = rows_.find(Key(id));
140 5 : if (it == rows_.end()) {
141 0 : return absl::NotFoundError("FakeStorage::ScanRows: table not registered");
142 0 : }
143 5 : return std::unique_ptr<storage::RowIterator>(
144 5 : new VectorIterator(it->second));
145 5 : }
146 : absl::StatusOr<std::unique_ptr<storage::RowIterator>> CreateReadStream(
147 : const storage::TableId& id,
148 0 : const storage::ReadFilter& /*filter*/) const override {
149 0 : return ScanRows(id);
150 0 : }
151 : absl::StatusOr<std::int64_t> CountRows(
152 0 : const storage::TableId& id) const override {
153 0 : auto it = rows_.find(Key(id));
154 0 : if (it == rows_.end()) {
155 0 : return absl::NotFoundError(
156 0 : "FakeStorage::CountRows: table not registered");
157 0 : }
158 0 : return static_cast<std::int64_t>(it->second.size());
159 0 : }
160 : absl::Status UpsertRoutine(
161 0 : const storage::RoutineRecord& /*record*/) override {
162 0 : return absl::UnimplementedError("FakeStorage::UpsertRoutine");
163 0 : }
164 0 : absl::Status DeleteRoutine(const storage::RoutineId& /*id*/) override {
165 0 : return absl::UnimplementedError("FakeStorage::DeleteRoutine");
166 0 : }
167 : absl::StatusOr<storage::RoutineRecord> GetRoutine(
168 0 : const storage::RoutineId& /*id*/) const override {
169 0 : return absl::UnimplementedError("FakeStorage::GetRoutine");
170 0 : }
171 : absl::StatusOr<std::vector<storage::RoutineRecord>> ListRoutines(
172 0 : const storage::DatasetId& /*dataset_id*/) const override {
173 0 : return absl::UnimplementedError("FakeStorage::ListRoutines");
174 0 : }
175 : absl::StatusOr<std::vector<storage::RoutineRecord>> ListAllRoutines()
176 0 : const override {
177 0 : return absl::UnimplementedError("FakeStorage::ListAllRoutines");
178 0 : }
179 0 : absl::Status UpsertView(const storage::ViewRecord&) override {
180 0 : return absl::UnimplementedError("FakeStorage::UpsertView");
181 0 : }
182 0 : absl::Status DeleteView(const storage::ViewId&) override {
183 0 : return absl::UnimplementedError("FakeStorage::DeleteView");
184 0 : }
185 : absl::StatusOr<std::vector<storage::ViewRecord>> ListAllViews()
186 0 : const override {
187 0 : return absl::UnimplementedError("FakeStorage::ListAllViews");
188 0 : }
189 :
190 : private:
191 47 : static std::string Key(const storage::TableId& id) {
192 47 : return id.project_id + "/" + id.dataset_id + "/" + id.table_id;
193 47 : }
194 :
195 : std::map<std::string, schema::TableSchema> schemas_;
196 : std::map<std::string, std::vector<storage::Row>> rows_;
197 : };
198 :
199 : // Test fixture: builds a `SimpleCatalog` that holds one
200 : // `StorageTable` (named "people") backed by `storage_`. Both the
201 : // columns the analyzer sees and the storage schema are identical:
202 : // `id INT64, name STRING`.
203 : class DmlExecutorTest : public ::testing::Test {
204 : protected:
205 9 : void SetUp() override {
206 9 : type_factory_ = std::make_unique<::googlesql::TypeFactory>();
207 9 : catalog_ = std::make_unique<::googlesql::SimpleCatalog>(
208 9 : "test_catalog", type_factory_.get());
209 9 : catalog_->AddBuiltinFunctions(
210 9 : ::googlesql::BuiltinFunctionOptions::AllReleasedFunctions());
211 :
212 9 : storage_ = std::make_unique<FakeStorage>();
213 9 : table_id_ = storage::TableId{"test_proj", "test_ds", "people"};
214 9 : schema::TableSchema schema;
215 9 : schema.columns.push_back({.name = "id",
216 9 : .type = schema::ColumnType::kInt64,
217 9 : .mode = schema::ColumnMode::kRequired});
218 9 : schema.columns.push_back({.name = "name",
219 9 : .type = schema::ColumnType::kString,
220 9 : .mode = schema::ColumnMode::kNullable});
221 9 : storage_->RegisterTable(table_id_, schema);
222 :
223 9 : std::vector<::googlesql::SimpleTable::NameAndType> cols = {
224 9 : {"id", type_factory_->get_int64()},
225 9 : {"name", type_factory_->get_string()},
226 9 : };
227 9 : auto storage_table = std::make_unique<catalog::StorageTable>(
228 9 : "people", "test_ds.people", cols, schema, table_id_, storage_.get());
229 : // The analyzer resolves multi-segment names by walking nested
230 : // catalogs. Surface the table under `test_ds.people` by attaching
231 : // it to a `test_ds` sub-catalog of the test root.
232 9 : ::googlesql::SimpleCatalog* dataset_catalog =
233 9 : catalog_->MakeOwnedSimpleCatalog("test_ds");
234 9 : dataset_catalog_ = dataset_catalog;
235 9 : dataset_catalog->AddOwnedTable(std::move(storage_table));
236 9 : }
237 :
238 14 : ::googlesql::AnalyzerOptions MakeOptions() {
239 14 : ::googlesql::LanguageOptions lang;
240 14 : lang.EnableMaximumLanguageFeatures();
241 14 : lang.set_product_mode(::googlesql::PRODUCT_EXTERNAL);
242 14 : lang.SetSupportsAllStatementKinds();
243 14 : ::googlesql::AnalyzerOptions opts(lang);
244 14 : opts.CreateDefaultArenasIfNotSet();
245 14 : return opts;
246 14 : }
247 :
248 14 : const ::googlesql::ResolvedStatement* Analyze(absl::string_view sql) {
249 14 : last_output_.reset();
250 14 : absl::Status s = ::googlesql::AnalyzeStatement(
251 14 : sql, MakeOptions(), catalog_.get(), type_factory_.get(), &last_output_);
252 28 : EXPECT_TRUE(s.ok()) << s;
253 14 : if (!s.ok() || last_output_ == nullptr) return nullptr;
254 14 : return last_output_->resolved_statement();
255 14 : }
256 :
257 : std::unique_ptr<::googlesql::TypeFactory> type_factory_{};
258 : std::unique_ptr<::googlesql::SimpleCatalog> catalog_{};
259 : std::unique_ptr<FakeStorage> storage_{};
260 : storage::TableId table_id_{};
261 : ::googlesql::SimpleCatalog* dataset_catalog_ = nullptr;
262 : std::unique_ptr<const ::googlesql::AnalyzerOutput> last_output_{};
263 : };
264 :
265 : // --- INSERT VALUES ---------------------------------------------------------
266 :
267 1 : TEST_F(DmlExecutorTest, InsertValuesAppendsRowsAndCountsThem) {
268 1 : const auto* stmt = Analyze(
269 1 : "INSERT INTO test_ds.people (id, name) VALUES "
270 1 : "(1, 'ada'), (2, 'linus'), (3, 'grace')");
271 1 : ASSERT_NE(stmt, nullptr);
272 1 : ASSERT_EQ(stmt->node_kind(), ::googlesql::RESOLVED_INSERT_STMT);
273 :
274 1 : QueryRequest request;
275 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
276 2 : ASSERT_TRUE(result.ok()) << result.status();
277 1 : EXPECT_EQ(result->stats.inserted_row_count, 3);
278 1 : EXPECT_EQ(result->stats.updated_row_count, 0);
279 1 : EXPECT_EQ(result->stats.deleted_row_count, 0);
280 :
281 1 : const auto& rows = storage_->Rows(table_id_);
282 1 : ASSERT_EQ(rows.size(), 3u);
283 1 : EXPECT_EQ(rows[0].cells[0].int64_value(), 1);
284 1 : EXPECT_EQ(rows[0].cells[1].string_value(), "ada");
285 1 : EXPECT_EQ(rows[1].cells[0].int64_value(), 2);
286 1 : EXPECT_EQ(rows[1].cells[1].string_value(), "linus");
287 1 : EXPECT_EQ(rows[2].cells[0].int64_value(), 3);
288 1 : EXPECT_EQ(rows[2].cells[1].string_value(), "grace");
289 1 : }
290 :
291 1 : TEST_F(DmlExecutorTest, InsertValuesOmittedColumnDefaultsNull) {
292 : // `INSERT (id) VALUES (1)` leaves `name` unbound; the executor
293 : // pads with NULL so the storage row matches the table schema.
294 1 : const auto* stmt = Analyze("INSERT INTO test_ds.people (id) VALUES (42)");
295 1 : ASSERT_NE(stmt, nullptr);
296 :
297 1 : QueryRequest request;
298 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
299 2 : ASSERT_TRUE(result.ok()) << result.status();
300 1 : EXPECT_EQ(result->stats.inserted_row_count, 1);
301 :
302 1 : const auto& rows = storage_->Rows(table_id_);
303 1 : ASSERT_EQ(rows.size(), 1u);
304 1 : EXPECT_EQ(rows[0].cells[0].int64_value(), 42);
305 1 : EXPECT_TRUE(rows[0].cells[1].is_null());
306 1 : }
307 :
308 1 : TEST_F(DmlExecutorTest, InsertSelectFromLiteralRows) {
309 1 : const auto* stmt = Analyze(
310 1 : "INSERT INTO test_ds.people (id, name) "
311 1 : "SELECT 1, 'a'");
312 1 : ASSERT_NE(stmt, nullptr);
313 :
314 1 : QueryRequest request;
315 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
316 2 : ASSERT_TRUE(result.ok()) << result.status();
317 1 : EXPECT_EQ(result->stats.inserted_row_count, 1);
318 :
319 1 : const auto& rows = storage_->Rows(table_id_);
320 1 : ASSERT_EQ(rows.size(), 1u);
321 1 : EXPECT_EQ(rows[0].cells[0].int64_value(), 1);
322 1 : EXPECT_EQ(rows[0].cells[1].string_value(), "a");
323 1 : }
324 :
325 : // --- DELETE -----------------------------------------------------------------
326 :
327 1 : TEST_F(DmlExecutorTest, DeleteWherePredicateRemovesMatchingRows) {
328 : // Seed via INSERT so we exercise the round-trip.
329 1 : {
330 1 : const auto* seed = Analyze(
331 1 : "INSERT INTO test_ds.people (id, name) "
332 1 : "VALUES (1, 'ada'), (2, 'linus'), (3, 'grace')");
333 1 : ASSERT_NE(seed, nullptr);
334 1 : QueryRequest req;
335 1 : ASSERT_TRUE(ExecuteDml(req, *seed, catalog_.get(), storage_.get()).ok());
336 1 : }
337 1 : ASSERT_EQ(storage_->Rows(table_id_).size(), 3u);
338 :
339 1 : const auto* stmt = Analyze("DELETE FROM test_ds.people WHERE id = 2");
340 1 : ASSERT_NE(stmt, nullptr);
341 1 : ASSERT_EQ(stmt->node_kind(), ::googlesql::RESOLVED_DELETE_STMT);
342 :
343 1 : QueryRequest request;
344 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
345 2 : ASSERT_TRUE(result.ok()) << result.status();
346 1 : EXPECT_EQ(result->stats.deleted_row_count, 1);
347 :
348 1 : const auto& rows = storage_->Rows(table_id_);
349 1 : ASSERT_EQ(rows.size(), 2u);
350 1 : EXPECT_EQ(rows[0].cells[0].int64_value(), 1);
351 1 : EXPECT_EQ(rows[1].cells[0].int64_value(), 3);
352 1 : }
353 :
354 1 : TEST_F(DmlExecutorTest, DeleteWhereTrueClearsTable) {
355 1 : {
356 1 : const auto* seed = Analyze(
357 1 : "INSERT INTO test_ds.people (id, name) "
358 1 : "VALUES (1, 'ada'), (2, 'linus')");
359 1 : QueryRequest req;
360 1 : ASSERT_TRUE(ExecuteDml(req, *seed, catalog_.get(), storage_.get()).ok());
361 1 : }
362 1 : const auto* stmt = Analyze("DELETE FROM test_ds.people WHERE TRUE");
363 1 : ASSERT_NE(stmt, nullptr);
364 1 : QueryRequest request;
365 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
366 2 : ASSERT_TRUE(result.ok()) << result.status();
367 1 : EXPECT_EQ(result->stats.deleted_row_count, 2);
368 1 : EXPECT_TRUE(storage_->Rows(table_id_).empty());
369 1 : }
370 :
371 : // --- UPDATE -----------------------------------------------------------------
372 :
373 1 : TEST_F(DmlExecutorTest, UpdateScalarSetMutatesMatchingRow) {
374 1 : {
375 1 : const auto* seed = Analyze(
376 1 : "INSERT INTO test_ds.people (id, name) "
377 1 : "VALUES (1, 'ada'), (2, 'linus'), (3, 'grace')");
378 1 : QueryRequest req;
379 1 : ASSERT_TRUE(ExecuteDml(req, *seed, catalog_.get(), storage_.get()).ok());
380 1 : }
381 1 : const auto* stmt =
382 1 : Analyze("UPDATE test_ds.people SET name = 'augusta' WHERE id = 1");
383 1 : ASSERT_NE(stmt, nullptr);
384 1 : ASSERT_EQ(stmt->node_kind(), ::googlesql::RESOLVED_UPDATE_STMT);
385 :
386 1 : QueryRequest request;
387 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
388 2 : ASSERT_TRUE(result.ok()) << result.status();
389 1 : EXPECT_EQ(result->stats.updated_row_count, 1);
390 :
391 1 : const auto& rows = storage_->Rows(table_id_);
392 1 : ASSERT_EQ(rows.size(), 3u);
393 1 : EXPECT_EQ(rows[0].cells[1].string_value(), "augusta");
394 1 : EXPECT_EQ(rows[1].cells[1].string_value(), "linus");
395 1 : EXPECT_EQ(rows[2].cells[1].string_value(), "grace");
396 1 : }
397 :
398 1 : TEST_F(DmlExecutorTest, UpdateSetExprUsesSourceColumnRef) {
399 1 : {
400 1 : const auto* seed = Analyze(
401 1 : "INSERT INTO test_ds.people (id, name) "
402 1 : "VALUES (1, 'ada')");
403 1 : QueryRequest req;
404 1 : ASSERT_TRUE(ExecuteDml(req, *seed, catalog_.get(), storage_.get()).ok());
405 1 : }
406 : // SET col = col + 10 -- exercises ColumnRef-rebinding inside the
407 : // SET expression evaluator.
408 1 : const auto* stmt =
409 1 : Analyze("UPDATE test_ds.people SET id = id + 10 WHERE id = 1");
410 1 : ASSERT_NE(stmt, nullptr);
411 :
412 1 : QueryRequest request;
413 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
414 2 : ASSERT_TRUE(result.ok()) << result.status();
415 1 : EXPECT_EQ(result->stats.updated_row_count, 1);
416 1 : EXPECT_EQ(storage_->Rows(table_id_)[0].cells[0].int64_value(), 11);
417 1 : }
418 :
419 1 : TEST_F(DmlExecutorTest, UpdateDeepStructSetMutatesNestedField) {
420 1 : storage::TableId struct_id{"test_proj", "test_ds", "items"};
421 1 : schema::TableSchema struct_schema;
422 1 : struct_schema.columns.push_back({.name = "id",
423 1 : .type = schema::ColumnType::kInt64,
424 1 : .mode = schema::ColumnMode::kRequired});
425 1 : schema::ColumnSchema nested_field;
426 1 : nested_field.name = "nested";
427 1 : nested_field.type = schema::ColumnType::kStruct;
428 1 : nested_field.mode = schema::ColumnMode::kNullable;
429 1 : nested_field.fields.push_back({.name = "val",
430 1 : .type = schema::ColumnType::kInt64,
431 1 : .mode = schema::ColumnMode::kNullable});
432 1 : nested_field.fields.push_back({.name = "tag",
433 1 : .type = schema::ColumnType::kString,
434 1 : .mode = schema::ColumnMode::kNullable});
435 1 : schema::ColumnSchema payload_col;
436 1 : payload_col.name = "payload";
437 1 : payload_col.type = schema::ColumnType::kStruct;
438 1 : payload_col.mode = schema::ColumnMode::kNullable;
439 1 : payload_col.fields.push_back(std::move(nested_field));
440 1 : struct_schema.columns.push_back(std::move(payload_col));
441 1 : storage_->RegisterTable(struct_id, struct_schema);
442 :
443 1 : const ::googlesql::StructType* inner_st = nullptr;
444 1 : ASSERT_TRUE(type_factory_
445 1 : ->MakeStructType({{"val", type_factory_->get_int64()},
446 1 : {"tag", type_factory_->get_string()}},
447 1 : &inner_st)
448 1 : .ok());
449 1 : const ::googlesql::StructType* payload_st = nullptr;
450 1 : ASSERT_TRUE(
451 1 : type_factory_->MakeStructType({{"nested", inner_st}}, &payload_st).ok());
452 1 : std::vector<::googlesql::SimpleTable::NameAndType> item_cols = {
453 1 : {"id", type_factory_->get_int64()},
454 1 : {"payload", payload_st},
455 1 : };
456 1 : auto item_table = std::make_unique<catalog::StorageTable>("items",
457 1 : "test_ds.items",
458 1 : item_cols,
459 1 : struct_schema,
460 1 : struct_id,
461 1 : storage_.get());
462 1 : dataset_catalog_->AddOwnedTable(std::move(item_table));
463 :
464 1 : const auto* seed = Analyze(
465 1 : "INSERT INTO test_ds.items (id, payload) VALUES "
466 1 : "(1, STRUCT(STRUCT(10 AS val, 'keep' AS tag) AS nested))");
467 1 : ASSERT_NE(seed, nullptr);
468 1 : QueryRequest req;
469 1 : ASSERT_TRUE(ExecuteDml(req, *seed, catalog_.get(), storage_.get()).ok());
470 :
471 1 : const auto* stmt =
472 1 : Analyze("UPDATE test_ds.items SET payload.nested.val = 99 WHERE id = 1");
473 1 : ASSERT_NE(stmt, nullptr);
474 1 : QueryRequest request;
475 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
476 2 : ASSERT_TRUE(result.ok()) << result.status();
477 1 : EXPECT_EQ(result->stats.updated_row_count, 1);
478 1 : }
479 :
480 1 : TEST_F(DmlExecutorTest, InsertAssertRowsModifiedMismatchRollsBack) {
481 1 : const auto* stmt = Analyze(
482 1 : "INSERT INTO test_ds.people (id, name) VALUES (1, 'ada') "
483 1 : "ASSERT_ROWS_MODIFIED 2");
484 1 : ASSERT_NE(stmt, nullptr);
485 :
486 1 : QueryRequest request;
487 1 : auto result = ExecuteDml(request, *stmt, catalog_.get(), storage_.get());
488 1 : ASSERT_FALSE(result.ok());
489 1 : EXPECT_NE(
490 1 : result.status().message().find("1 row(s), but 2 row(s) were expected"),
491 1 : std::string::npos);
492 1 : EXPECT_TRUE(storage_->Rows(table_id_).empty());
493 1 : }
494 :
495 : } // namespace
496 : } // namespace dml
497 : } // namespace semantic
498 : } // namespace engine
499 : } // namespace backend
500 : } // namespace bigquery_emulator
|