LCOV - code coverage report
Current view: top level - backend/catalog - udf_registry_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 93 93
Test Date: 2026-07-02 21:01:18 Functions: 100.0 % 4 4

            Line data    Source code
       1              : // Regression tests for the UDF registry's object lifetime contract.
       2              : //
       3              : // Catalogs (notably the long-lived per-project registration catalog in
       4              : // udf_registration_catalog.cc) hold raw `googlesql::Function*` pointers
       5              : // handed out via SimpleCatalog::AddFunction. Re-registering a function
       6              : // (the routine-update / upsert path) used to destroy the old object,
       7              : // leaving those raw pointers dangling; the next
       8              : // ReplayFunctionsIntoCatalog dereferenced them and crashed the engine
       9              : // (use-after-free surfacing as an InsertOrDie duplicate-key abort or a
      10              : // SIGSEGV). See the node-bigquery-tests "Delete Routine" before-all
      11              : // hook failure in CI.
      12              : 
      13              : #include "backend/catalog/udf_registry.h"
      14              : 
      15              : #include <memory>
      16              : #include <string>
      17              : #include <utility>
      18              : #include <vector>
      19              : 
      20              : #include "googlesql/public/function.h"
      21              : #include "googlesql/public/function_signature.h"
      22              : #include "googlesql/public/simple_catalog.h"
      23              : #include "googlesql/public/types/type_factory.h"
      24              : #include "gtest/gtest.h"
      25              : 
      26              : namespace bigquery_emulator {
      27              : namespace backend {
      28              : namespace catalog {
      29              : namespace {
      30              : 
      31              : std::unique_ptr<const ::googlesql::Function> MakeScalarFn(
      32            4 :     const std::string& name) {
      33            4 :   ::googlesql::FunctionSignature signature(
      34            4 :       ::googlesql::FunctionArgumentType(::googlesql::types::Int64Type()),
      35            4 :       /*arguments=*/{},
      36            4 :       /*context_id=*/static_cast<int64_t>(0));
      37            4 :   return std::make_unique<::googlesql::Function>(
      38            4 :       std::vector<std::string>{name},
      39            4 :       /*group=*/"External_function",
      40            4 :       ::googlesql::Function::SCALAR,
      41            4 :       std::vector<::googlesql::FunctionSignature>{signature});
      42            4 : }
      43              : 
      44            1 : TEST(UdfRegistryTest, ReRegisterKeepsOldPointerValidAndReplaysNewFunction) {
      45              :   // Distinct project id: the registry is process-global state.
      46            1 :   const std::string project = "udf_registry_test_reregister";
      47            1 :   const std::string fn_name = "regtest_ds.fn1";
      48              : 
      49            1 :   ::googlesql::TypeFactory type_factory;
      50            1 :   ::googlesql::SimpleCatalog catalog(project, &type_factory);
      51              : 
      52            1 :   ASSERT_TRUE(RegisterProjectFunction(project,
      53            1 :                                       /*dataset_id=*/"",
      54            1 :                                       /*is_temp=*/false,
      55            1 :                                       /*analyzer_output=*/nullptr,
      56            1 :                                       MakeScalarFn(fn_name))
      57            1 :                   .ok());
      58            1 :   ReplayFunctionsIntoCatalog(project, catalog);
      59              : 
      60            1 :   const ::googlesql::Function* first = nullptr;
      61            1 :   ASSERT_TRUE(catalog.GetFunction(fn_name, &first).ok());
      62            1 :   ASSERT_NE(first, nullptr);
      63              : 
      64              :   // Routine update path: re-register the same name. The old object
      65              :   // must stay alive because `catalog` still holds a raw pointer.
      66            1 :   ASSERT_TRUE(RegisterProjectFunction(project,
      67            1 :                                       /*dataset_id=*/"",
      68            1 :                                       /*is_temp=*/false,
      69            1 :                                       /*analyzer_output=*/nullptr,
      70            1 :                                       MakeScalarFn(fn_name))
      71            1 :                   .ok());
      72            1 :   EXPECT_EQ(first->Name(), fn_name);  // dereference: dies pre-fix under ASAN
      73              : 
      74              :   // Replay must replace the catalog entry with the new registration
      75              :   // instead of aborting on a duplicate key.
      76            1 :   ReplayFunctionsIntoCatalog(project, catalog);
      77            1 :   const ::googlesql::Function* second = nullptr;
      78            1 :   ASSERT_TRUE(catalog.GetFunction(fn_name, &second).ok());
      79            1 :   ASSERT_NE(second, nullptr);
      80            1 :   EXPECT_NE(second, first);
      81            1 :   EXPECT_TRUE(IsProjectRegisteredFunction(project, fn_name));
      82            1 : }
      83              : 
      84            1 : TEST(UdfRegistryTest, DropRemovesFunctionFromCatalogOnNextReplay) {
      85            1 :   const std::string project = "udf_registry_test_drop";
      86            1 :   const std::string fn_name = "regtest_ds.fn2";
      87              : 
      88            1 :   ::googlesql::TypeFactory type_factory;
      89            1 :   ::googlesql::SimpleCatalog catalog(project, &type_factory);
      90              : 
      91            1 :   ASSERT_TRUE(RegisterProjectFunction(project,
      92            1 :                                       /*dataset_id=*/"",
      93            1 :                                       /*is_temp=*/false,
      94            1 :                                       /*analyzer_output=*/nullptr,
      95            1 :                                       MakeScalarFn(fn_name))
      96            1 :                   .ok());
      97            1 :   ReplayFunctionsIntoCatalog(project, catalog);
      98              : 
      99            1 :   const ::googlesql::Function* registered = nullptr;
     100            1 :   ASSERT_TRUE(catalog.GetFunction(fn_name, &registered).ok());
     101            1 :   ASSERT_NE(registered, nullptr);
     102              : 
     103            1 :   ASSERT_TRUE(DropProjectFunction(project, fn_name).ok());
     104            1 :   EXPECT_FALSE(IsProjectRegisteredFunction(project, fn_name));
     105              : 
     106              :   // The dropped function is retired, not destroyed, and the next
     107              :   // replay purges the stale catalog entry.
     108            1 :   ReplayFunctionsIntoCatalog(project, catalog);
     109            1 :   const ::googlesql::Function* after_drop = nullptr;
     110            1 :   ASSERT_TRUE(catalog.GetFunction(fn_name, &after_drop).ok());
     111            1 :   EXPECT_EQ(after_drop, nullptr);
     112            1 : }
     113              : 
     114            1 : TEST(UdfRegistryTest, FindProjectFunctionResolvesDatasetQualifiedRoutine) {
     115            1 :   const std::string project = "udf_registry_test_qualified";
     116            1 :   const std::string dataset = "ds_udf";
     117            1 :   const std::string routine = "add_one";
     118              : 
     119            1 :   ::googlesql::TypeFactory type_factory;
     120            1 :   ::googlesql::SimpleCatalog catalog(project, &type_factory);
     121              : 
     122            1 :   ASSERT_TRUE(RegisterProjectFunction(project,
     123            1 :                                       dataset,
     124            1 :                                       /*is_temp=*/false,
     125            1 :                                       /*analyzer_output=*/nullptr,
     126            1 :                                       MakeScalarFn(routine))
     127            1 :                   .ok());
     128            1 :   ReplayFunctionsIntoCatalog(project, catalog);
     129              : 
     130            1 :   const ::googlesql::Function* by_short = nullptr;
     131            1 :   ASSERT_TRUE(catalog.GetFunction(routine, &by_short).ok());
     132            1 :   ASSERT_NE(by_short, nullptr);
     133              : 
     134            1 :   const ::googlesql::Function* qualified =
     135            1 :       FindProjectFunction(project, dataset, routine);
     136            1 :   ASSERT_NE(qualified, nullptr);
     137            1 :   EXPECT_EQ(qualified, by_short);
     138              : 
     139            1 :   EXPECT_EQ(FindProjectFunction(project, "other_ds", routine), nullptr);
     140            1 :   EXPECT_EQ(FindProjectFunction(project, dataset, "missing"), nullptr);
     141              : 
     142            1 :   const ::googlesql::Function* from_path = FindProjectFunctionFromPath(
     143            1 :       {project, dataset, routine}, project, dataset);
     144            1 :   ASSERT_NE(from_path, nullptr);
     145            1 :   EXPECT_EQ(from_path, qualified);
     146              : 
     147            1 :   const ::googlesql::Function* dotted = FindProjectFunctionFromPath(
     148            1 :       {project + "." + dataset + "." + routine}, project, dataset);
     149            1 :   ASSERT_NE(dotted, nullptr);
     150            1 :   EXPECT_EQ(dotted, qualified);
     151            1 : }
     152              : 
     153              : }  // namespace
     154              : }  // namespace catalog
     155              : }  // namespace backend
     156              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1