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(), ®_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
|