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
|