LCOV - code coverage report
Current view: top level - backend/engine/duckdb/udf/numeric - numeric_macros_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 79.5 % 117 93
Test Date: 2026-07-02 21:01:18 Functions: 100.0 % 17 17

            Line data    Source code
       1              : // Unit tests for the BigQuery numeric polyfill macros.
       2              : //
       3              : // Each test drives the macro directly against an in-process DuckDB
       4              : // connection (mirroring the per-query connection lifecycle
       5              : // `DuckDbExecutor` uses) and exercises both the common path and the
       6              : // BigQuery-specific edge case the wrapper exists to pin.
       7              : 
       8              : #include <cstdint>
       9              : #include <string>
      10              : 
      11              : #include "absl/status/status.h"
      12              : #include "backend/engine/duckdb/udf/registrar.h"
      13              : #include "duckdb.h"
      14              : #include "gtest/gtest.h"
      15              : 
      16              : namespace bigquery_emulator {
      17              : namespace backend {
      18              : namespace engine {
      19              : namespace duckdb {
      20              : namespace udf {
      21              : namespace {
      22              : 
      23              : // Test fixture that owns a fresh, in-memory DuckDB connection with
      24              : // the polyfill UDF library registered. Identical to the executor's
      25              : // per-query setup; per-UDF tests run against the same surface.
      26              : class NumericMacrosTest : public ::testing::Test {
      27              :  protected:
      28           11 :   void SetUp() override {
      29           11 :     ASSERT_EQ(::duckdb_open(nullptr, &db_), ::DuckDBSuccess);
      30           11 :     ASSERT_EQ(::duckdb_connect(db_, &conn_), ::DuckDBSuccess);
      31           11 :     absl::Status reg = RegisterAll(conn_);
      32           22 :     ASSERT_TRUE(reg.ok()) << reg;
      33           11 :   }
      34              : 
      35           11 :   void TearDown() override {
      36           11 :     if (conn_ != nullptr) ::duckdb_disconnect(&conn_);
      37           11 :     if (db_ != nullptr) ::duckdb_close(&db_);
      38           11 :   }
      39              : 
      40              :   // Runs `sql` and returns a single INT64 from row 0, column 0.
      41              :   // Fails the test if the query rejected or returned no rows.
      42           14 :   int64_t RunInt64(const std::string& sql) {
      43           14 :     ::duckdb_result result;
      44           14 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      45           14 :     if (rc != ::DuckDBSuccess) {
      46            0 :       ADD_FAILURE() << "DuckDB rejected: "
      47            0 :                     << (::duckdb_result_error(&result) == nullptr
      48            0 :                             ? "(no error string)"
      49            0 :                             : ::duckdb_result_error(&result))
      50            0 :                     << " (sql=" << sql << ")";
      51            0 :       ::duckdb_destroy_result(&result);
      52            0 :       return INT64_MIN;
      53            0 :     }
      54           14 :     int64_t value = ::duckdb_value_int64(&result, 0, 0);
      55           14 :     ::duckdb_destroy_result(&result);
      56           14 :     return value;
      57           14 :   }
      58              : 
      59              :   // Returns true when the cell at (0, 0) is NULL. NULL-aware
      60              :   // helper because `duckdb_value_int64` returns 0 for a NULL cell
      61              :   // (no way to distinguish "literal 0" from "NULL" without this).
      62            8 :   bool RunIsNull(const std::string& sql) {
      63            8 :     ::duckdb_result result;
      64            8 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      65            8 :     if (rc != ::DuckDBSuccess) {
      66            0 :       ADD_FAILURE() << "DuckDB rejected: "
      67            0 :                     << (::duckdb_result_error(&result) == nullptr
      68            0 :                             ? "(no error string)"
      69            0 :                             : ::duckdb_result_error(&result))
      70            0 :                     << " (sql=" << sql << ")";
      71            0 :       ::duckdb_destroy_result(&result);
      72            0 :       return false;
      73            0 :     }
      74            8 :     bool is_null = ::duckdb_value_is_null(&result, 0, 0);
      75            8 :     ::duckdb_destroy_result(&result);
      76            8 :     return is_null;
      77            8 :   }
      78              : 
      79              :   // Returns true iff `sql` was rejected by DuckDB. Used to assert
      80              :   // that non-SAFE divide-by-zero raises (matching BigQuery).
      81            2 :   bool RunRejects(const std::string& sql) {
      82            2 :     ::duckdb_result result;
      83            2 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      84            2 :     ::duckdb_destroy_result(&result);
      85            2 :     return rc != ::DuckDBSuccess;
      86            2 :   }
      87              : 
      88              :   // Runs `sql` and returns a DOUBLE from row 0, column 0.
      89            5 :   double RunDouble(const std::string& sql) {
      90            5 :     ::duckdb_result result;
      91            5 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      92            5 :     if (rc != ::DuckDBSuccess) {
      93            0 :       ADD_FAILURE() << "DuckDB rejected: "
      94            0 :                     << (::duckdb_result_error(&result) == nullptr
      95            0 :                             ? "(no error string)"
      96            0 :                             : ::duckdb_result_error(&result))
      97            0 :                     << " (sql=" << sql << ")";
      98            0 :       ::duckdb_destroy_result(&result);
      99            0 :       return 0.0;
     100            0 :     }
     101            5 :     double value = ::duckdb_value_double(&result, 0, 0);
     102            5 :     ::duckdb_destroy_result(&result);
     103            5 :     return value;
     104            5 :   }
     105              : 
     106              :   ::duckdb_database db_ = nullptr;
     107              :   ::duckdb_connection conn_ = nullptr;
     108              : };
     109              : 
     110              : // --- bq_mod ------------------------------------------------------
     111              : 
     112            1 : TEST_F(NumericMacrosTest, ModCommonPath) {
     113            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(7, 3)"), 1);
     114            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(6, 3)"), 0);
     115            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(0, 5)"), 0);
     116            1 : }
     117              : 
     118            1 : TEST_F(NumericMacrosTest, ModSignTracksDividend) {
     119              :   // Edge case pinned: BigQuery MOD's sign follows the dividend
     120              :   // (truncated division), not the divisor (floor / Python-like).
     121              :   // A regression switching to floor-mod would surface here.
     122            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(-7, 3)"), -1);
     123            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(7, -3)"), 1);
     124            1 :   EXPECT_EQ(RunInt64("SELECT bq_mod(-7, -3)"), -1);
     125            1 : }
     126              : 
     127            1 : TEST_F(NumericMacrosTest, ModNullPropagation) {
     128            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_mod(NULL::BIGINT, 3)"));
     129            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_mod(7, NULL::BIGINT)"));
     130            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_mod(NULL::BIGINT, NULL::BIGINT)"));
     131            1 : }
     132              : 
     133            1 : TEST_F(NumericMacrosTest, ModByZeroRaises) {
     134              :   // BigQuery non-SAFE MOD raises on Y=0; DuckDB raises on integer
     135              :   // `% 0`. The macro inherits that behavior.
     136            1 :   EXPECT_TRUE(RunRejects("SELECT bq_mod(7, 0)"));
     137            1 : }
     138              : 
     139              : // --- bq_div ------------------------------------------------------
     140              : 
     141            1 : TEST_F(NumericMacrosTest, DivCommonPath) {
     142            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(7, 3)"), 2);
     143            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(6, 3)"), 2);
     144            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(0, 5)"), 0);
     145            1 : }
     146              : 
     147            1 : TEST_F(NumericMacrosTest, DivTruncatesNotFloors) {
     148              :   // Edge case pinned: BigQuery DIV truncates toward zero. DuckDB's
     149              :   // bare `//` is FLOOR division, which would give -3 for
     150              :   // `-5 // 2`. The macro restores truncation through the
     151              :   // `(x - x % y) / y` identity. A regression that swapped the
     152              :   // macro body back to `x // y` would surface as -3 here.
     153            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(-5, 2)"), -2);
     154            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(5, -2)"), -2);
     155            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(-5, -2)"), 2);
     156            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(-6, 2)"), -3);
     157            1 :   EXPECT_EQ(RunInt64("SELECT bq_div(6, -2)"), -3);
     158            1 : }
     159              : 
     160            1 : TEST_F(NumericMacrosTest, DivNullPropagation) {
     161            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_div(NULL::BIGINT, 3)"));
     162            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_div(7, NULL::BIGINT)"));
     163            1 : }
     164              : 
     165            1 : TEST_F(NumericMacrosTest, DivByZeroRaises) {
     166            1 :   EXPECT_TRUE(RunRejects("SELECT bq_div(7, 0)"));
     167            1 : }
     168              : 
     169              : // --- bq_log ------------------------------------------------------
     170              : 
     171            1 : TEST_F(NumericMacrosTest, LogSingleArgIsNaturalLog) {
     172              :   // Edge case pinned: BigQuery LOG(x) returns the NATURAL log,
     173              :   // unlike DuckDB's bare `log(x)` which returns base-10. The macro
     174              :   // routes to `ln(x)`; a regression that swapped to `log10(x)`
     175              :   // would surface here as ~2.302585... instead of ~2.302585...
     176              :   // (wait -- log10(10) == 1 and ln(10) == 2.302585...; the literal
     177              :   // 10 was deliberately picked because the two diverge there).
     178            1 :   EXPECT_NEAR(RunDouble("SELECT bq_log(1.0::DOUBLE)"), 0.0, 1e-9);
     179            1 :   EXPECT_NEAR(
     180            1 :       RunDouble("SELECT bq_log(10.0::DOUBLE)"), 2.302585092994046, 1e-9);
     181            1 : }
     182              : 
     183            1 : TEST_F(NumericMacrosTest, LogTwoArgIdentity) {
     184              :   // Edge case pinned: BigQuery LOG(X, Y) returns log_Y(X) with X
     185              :   // as the FIRST argument; DuckDB's two-arg `log(b, x)` has the
     186              :   // base FIRST. The macro re-derives through `ln(x) / ln(base)`
     187              :   // so the argument order matches BigQuery regardless of how
     188              :   // DuckDB's two-arg log is wired.
     189            1 :   EXPECT_NEAR(
     190            1 :       RunDouble("SELECT bq_log(100.0::DOUBLE, 10.0::DOUBLE)"), 2.0, 1e-9);
     191            1 :   EXPECT_NEAR(RunDouble("SELECT bq_log(8.0::DOUBLE, 2.0::DOUBLE)"), 3.0, 1e-9);
     192            1 :   EXPECT_NEAR(RunDouble("SELECT bq_log(27.0::DOUBLE, 3.0::DOUBLE)"), 3.0, 1e-9);
     193            1 : }
     194              : 
     195            1 : TEST_F(NumericMacrosTest, LogNullPropagation) {
     196            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_log(NULL::DOUBLE)"));
     197            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_log(NULL::DOUBLE, 10.0::DOUBLE)"));
     198            1 :   EXPECT_TRUE(RunIsNull("SELECT bq_log(100.0::DOUBLE, NULL::DOUBLE)"));
     199            1 : }
     200              : 
     201              : }  // namespace
     202              : }  // namespace udf
     203              : }  // namespace duckdb
     204              : }  // namespace engine
     205              : }  // namespace backend
     206              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1