LCOV - code coverage report
Current view: top level - catalog - catalog_crash_safety_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 96.2 % 159 153
Test Date: 2026-08-08 08:38:15 Functions: 100.0 % 11 11

            Line data    Source code
       1              : // Crash-safety regression tests for client-reachable catalog paths.
       2              : //
       3              : // R4: Engine abort on duplicate catalog name during view replay (authorize-view
       4              : // repeat). A duplicate bare view name across datasets used to abort the engine
       5              : // via SimpleCatalog::AddTable during eager view replay. Views now resolve
       6              : // lazily through FindProjectView; these tests assert catalog construction and
       7              : // registration never abort on adversarial duplicate names or replay cycles.
       8              : //
       9              : // Indexed in conformance/REGRESSIONS.md; crash-safety posture in
      10              : // .cursor/rules/no-fatal-on-client-state.mdc.
      11              : 
      12              : #include <cstdlib>
      13              : #include <filesystem>
      14              : #include <memory>
      15              : #include <random>
      16              : #include <string>
      17              : #include <system_error>
      18              : #include <utility>
      19              : 
      20              : #include "absl/status/status.h"
      21              : #include "absl/strings/str_cat.h"
      22              : #include "backend/catalog/googlesql_catalog.h"
      23              : #include "backend/catalog/udf_registration_catalog.h"
      24              : #include "backend/catalog/udf_registry.h"
      25              : #include "backend/catalog/view_registry.h"
      26              : #include "backend/schema/schema.h"
      27              : #include "backend/storage/duckdb/duckdb_storage.h"
      28              : #include "googlesql/public/analyzer.h"
      29              : #include "googlesql/public/analyzer_options.h"
      30              : #include "googlesql/public/analyzer_output.h"
      31              : #include "googlesql/public/function.h"
      32              : #include "googlesql/public/function_signature.h"
      33              : #include "googlesql/public/language_options.h"
      34              : #include "googlesql/public/options.pb.h"
      35              : #include "googlesql/public/simple_catalog.h"
      36              : #include "googlesql/public/types/type_factory.h"
      37              : #include "googlesql/resolved_ast/resolved_ast.h"
      38              : #include "googlesql/resolved_ast/resolved_node_kind.pb.h"
      39              : #include "gtest/gtest.h"
      40              : 
      41              : namespace bigquery_emulator {
      42              : namespace backend {
      43              : namespace catalog {
      44              : namespace {
      45              : 
      46              : namespace fs = std::filesystem;
      47              : 
      48              : const char* kProject = "proj_catalog_crash_safety";
      49              : 
      50           31 : ::googlesql::LanguageOptions MakeLanguageOptions() {
      51           31 :   ::googlesql::LanguageOptions language;
      52           31 :   language.EnableMaximumLanguageFeatures();
      53           31 :   language.set_product_mode(::googlesql::PRODUCT_EXTERNAL);
      54           31 :   language.set_name_resolution_mode(::googlesql::NAME_RESOLUTION_DEFAULT);
      55           31 :   return language;
      56           31 : }
      57              : 
      58            9 : ::googlesql::AnalyzerOptions MakeAnalyzerOptions() {
      59            9 :   ::googlesql::AnalyzerOptions options(MakeLanguageOptions());
      60            9 :   options.set_error_message_mode(::googlesql::ERROR_MESSAGE_ONE_LINE);
      61            9 :   options.CreateDefaultArenasIfNotSet();
      62            9 :   options.mutable_language()->SetSupportsAllStatementKinds();
      63            9 :   return options;
      64            9 : }
      65              : 
      66              : std::unique_ptr<const ::googlesql::Function> MakeScalarFn(
      67            1 :     const std::string& name) {
      68            1 :   ::googlesql::FunctionSignature signature(
      69            1 :       ::googlesql::FunctionArgumentType(::googlesql::types::Int64Type()),
      70            1 :       /*arguments=*/{},
      71            1 :       /*context_id=*/static_cast<int64_t>(0));
      72            1 :   return std::make_unique<::googlesql::Function>(
      73            1 :       std::vector<std::string>{name},
      74            1 :       /*group=*/"External_function",
      75            1 :       ::googlesql::Function::SCALAR,
      76            1 :       std::vector<::googlesql::FunctionSignature>{signature});
      77            1 : }
      78              : 
      79              : class CatalogCrashSafetyTest : public ::testing::Test {
      80              :  protected:
      81            4 :   void SetUp() override {
      82            4 :     const char* tmpdir_env = std::getenv("TMPDIR");
      83            4 :     const std::string tmpdir = tmpdir_env != nullptr ? tmpdir_env : "/tmp";
      84            4 :     std::random_device rd;
      85            4 :     std::seed_seq seed{rd(), rd()};
      86            4 :     std::mt19937_64 rng(seed);
      87            4 :     data_dir_ =
      88            4 :         fs::path(tmpdir) / absl::StrCat("bqemu-catalog-crash-safety-", rng());
      89            4 :     std::error_code ec;
      90            4 :     fs::remove_all(data_dir_, ec);
      91            4 :     auto opened = storage::duckdb::DuckDBStorage::Open(data_dir_.string());
      92            8 :     ASSERT_TRUE(opened.ok()) << opened.status();
      93            4 :     storage_ = std::move(opened).value();
      94            4 :     ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_base"}, "US").ok());
      95            4 :     schema::TableSchema schema;
      96            4 :     schema.columns.push_back({.name = "id",
      97            4 :                               .type = schema::ColumnType::kInt64,
      98            4 :                               .mode = schema::ColumnMode::kRequired});
      99            4 :     ASSERT_TRUE(
     100            4 :         storage_->CreateTable({kProject, "ds_base", "source"}, schema).ok());
     101            4 :   }
     102              : 
     103            4 :   void TearDown() override {
     104            4 :     storage_.reset();
     105            4 :     std::error_code ec;
     106            4 :     fs::remove_all(data_dir_, ec);
     107            4 :   }
     108              : 
     109              :   struct CatalogBundle {
     110              :     std::unique_ptr<::googlesql::TypeFactory> type_factory{};
     111              :     std::unique_ptr<GoogleSqlCatalog> catalog{};
     112              :   };
     113              : 
     114           22 :   CatalogBundle MakeCatalog(absl::string_view default_dataset = "") {
     115           22 :     auto type_factory = std::make_unique<::googlesql::TypeFactory>();
     116           22 :     auto catalog = std::make_unique<GoogleSqlCatalog>(kProject,
     117           22 :                                                       storage_.get(),
     118           22 :                                                       type_factory.get(),
     119           22 :                                                       MakeLanguageOptions(),
     120           22 :                                                       default_dataset);
     121           22 :     return {std::move(type_factory), std::move(catalog)};
     122           22 :   }
     123              : 
     124            9 :   absl::Status RegisterViewFromSql(absl::string_view sql) {
     125            9 :     CatalogBundle bundle = MakeCatalog();
     126            9 :     ::googlesql::TypeFactory analyze_tf;
     127            9 :     std::unique_ptr<const ::googlesql::AnalyzerOutput> output;
     128            9 :     absl::Status analyzed = ::googlesql::AnalyzeStatement(
     129            9 :         sql, MakeAnalyzerOptions(), bundle.catalog.get(), &analyze_tf, &output);
     130            9 :     if (!analyzed.ok()) return analyzed;
     131            9 :     const ::googlesql::ResolvedStatement* stmt = output->resolved_statement();
     132            9 :     if (stmt == nullptr) {
     133            0 :       return absl::InternalError("analyzer returned null statement");
     134            0 :     }
     135            9 :     if (stmt->node_kind() != ::googlesql::RESOLVED_CREATE_VIEW_STMT) {
     136            0 :       return absl::InvalidArgumentError("expected CREATE VIEW statement");
     137            0 :     }
     138            9 :     const auto* create_view =
     139            9 :         stmt->GetAs<::googlesql::ResolvedCreateViewStmt>();
     140            9 :     if (create_view == nullptr) {
     141            0 :       return absl::InternalError("CREATE VIEW has null resolved stmt");
     142            0 :     }
     143            9 :     ::googlesql::TypeFactory* reg_tf = EnsureProjectTypeFactory(kProject);
     144            9 :     return RegisterProjectView(kProject,
     145            9 :                                /*default_dataset_id=*/"",
     146            9 :                                *create_view,
     147            9 :                                std::move(output),
     148            9 :                                reg_tf);
     149            9 :   }
     150              : 
     151              :   fs::path data_dir_{};
     152              :   std::unique_ptr<storage::duckdb::DuckDBStorage> storage_{};
     153              : };
     154              : 
     155              : TEST_F(CatalogCrashSafetyTest,
     156            1 :        DuplicateViewNameAcrossDatasetsDoesNotAbortCatalogConstruction) {
     157            1 :   ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_a"}, "US").ok());
     158            1 :   ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_b"}, "US").ok());
     159              : 
     160            1 :   ASSERT_TRUE(RegisterViewFromSql(
     161            1 :                   "CREATE VIEW ds_a.profiles AS SELECT id FROM ds_base.source")
     162            1 :                   .ok());
     163            1 :   ASSERT_TRUE(RegisterViewFromSql(
     164            1 :                   "CREATE VIEW ds_b.profiles AS SELECT id FROM ds_base.source")
     165            1 :                   .ok());
     166              : 
     167            6 :   for (int i = 0; i < 5; ++i) {
     168            5 :     CatalogBundle bundle = MakeCatalog();
     169            5 :     const ::googlesql::Table* table_a = nullptr;
     170            5 :     const ::googlesql::Table* table_b = nullptr;
     171            5 :     EXPECT_TRUE(bundle.catalog->FindTable({"ds_a", "profiles"}, &table_a).ok());
     172            5 :     EXPECT_TRUE(bundle.catalog->FindTable({"ds_b", "profiles"}, &table_b).ok());
     173            5 :     ASSERT_NE(table_a, nullptr);
     174            5 :     ASSERT_NE(table_b, nullptr);
     175            5 :     EXPECT_NE(table_a, table_b);
     176            5 :   }
     177            1 : }
     178              : 
     179            1 : TEST_F(CatalogCrashSafetyTest, ReRegisterViewReplacesWithoutAbort) {
     180            1 :   ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_tenant"}, "US").ok());
     181            1 :   const std::string ddl =
     182            1 :       "CREATE VIEW ds_tenant.v AS SELECT id FROM ds_base.source";
     183            1 :   ASSERT_TRUE(RegisterViewFromSql(ddl).ok());
     184            1 :   ASSERT_TRUE(
     185            1 :       RegisterViewFromSql(
     186            1 :           "CREATE OR REPLACE VIEW ds_tenant.v AS SELECT id FROM ds_base.source")
     187            1 :           .ok());
     188              : 
     189            4 :   for (int i = 0; i < 3; ++i) {
     190            3 :     CatalogBundle bundle = MakeCatalog();
     191            3 :     const ::googlesql::Table* view = nullptr;
     192            3 :     EXPECT_TRUE(bundle.catalog->FindTable({"ds_tenant", "v"}, &view).ok());
     193            3 :     ASSERT_NE(view, nullptr);
     194            3 :   }
     195            1 : }
     196              : 
     197            1 : TEST_F(CatalogCrashSafetyTest, ReplayFunctionsTwiceOnSameCatalogDoesNotAbort) {
     198            1 :   const std::string fn_name = "crash_safety_ds.fn";
     199            1 :   ::googlesql::TypeFactory type_factory;
     200            1 :   ::googlesql::SimpleCatalog catalog(kProject, &type_factory);
     201              : 
     202            1 :   ASSERT_TRUE(RegisterProjectFunction(kProject,
     203            1 :                                       /*dataset_id=*/"",
     204            1 :                                       /*is_temp=*/false,
     205            1 :                                       /*analyzer_output=*/nullptr,
     206            1 :                                       MakeScalarFn(fn_name))
     207            1 :                   .ok());
     208            1 :   ReplayFunctionsIntoCatalog(kProject, catalog);
     209            1 :   ReplayFunctionsIntoCatalog(kProject, catalog);
     210              : 
     211            1 :   const ::googlesql::Function* fn = nullptr;
     212            1 :   ASSERT_TRUE(catalog.GetFunction(fn_name, &fn).ok());
     213            1 :   ASSERT_NE(fn, nullptr);
     214            1 : }
     215              : 
     216              : TEST_F(CatalogCrashSafetyTest,
     217            1 :        RegistrationCatalogSurvivesRepeatedViewAuthorizeCycles) {
     218            1 :   ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_main"}, "US").ok());
     219            1 :   ASSERT_TRUE(storage_->CreateDataset({kProject, "ds_tenant"}, "US").ok());
     220              : 
     221            1 :   ::googlesql::TypeFactory reg_tf;
     222            1 :   const ::googlesql::LanguageOptions language = MakeCatalogLanguageOptions();
     223              : 
     224            6 :   for (int i = 0; i < 5; ++i) {
     225            5 :     ASSERT_TRUE(RegisterViewFromSql(
     226            5 :                     "CREATE OR REPLACE VIEW ds_tenant.v AS SELECT id FROM "
     227            5 :                     "ds_base.source")
     228            5 :                     .ok());
     229            5 :     GoogleSqlCatalog* reg_catalog = nullptr;
     230            5 :     reg_catalog = GetOrCreateRegistrationCatalog(
     231            5 :         kProject, storage_.get(), &reg_tf, language, "ds_tenant");
     232            5 :     ASSERT_NE(reg_catalog, nullptr);
     233              : 
     234            5 :     CatalogBundle query_catalog = MakeCatalog("ds_tenant");
     235            5 :     const ::googlesql::Table* view = nullptr;
     236            5 :     EXPECT_TRUE(
     237            5 :         query_catalog.catalog->FindTable({"ds_tenant", "v"}, &view).ok());
     238            5 :     ASSERT_NE(view, nullptr);
     239            5 :   }
     240            1 : }
     241              : 
     242              : }  // namespace
     243              : }  // namespace catalog
     244              : }  // namespace backend
     245              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1