LCOV - code coverage report
Current view: top level - backend/engine/semantic/dml - dml_executor_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 79.8 % 342 273
Test Date: 2026-07-02 21:01:18 Functions: 54.1 % 37 20

            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
        

Generated by: LCOV version 2.0-1