LCOV - code coverage report
Current view: top level - engine/duckdb/udf/datetime - datetime_macros_test.cc (source / functions) Coverage Total Hit
Test: _coverage_report.dat Lines: 100.0 % 137 137
Test Date: 2026-08-08 08:38:15 Functions: 100.0 % 27 27

            Line data    Source code
       1              : // Unit tests for the BigQuery datetime polyfill macros.
       2              : //
       3              : // Each test drives the macro directly against an in-process DuckDB
       4              : // connection and exercises both the common path and the
       5              : // BigQuery-specific edge case the wrapper exists to pin.
       6              : 
       7              : #include <cstdint>
       8              : #include <string>
       9              : 
      10              : #include "absl/status/status.h"
      11              : #include "backend/engine/duckdb/udf/registrar.h"
      12              : #include "duckdb.h"
      13              : #include "gtest/gtest.h"
      14              : 
      15              : namespace bigquery_emulator {
      16              : namespace backend {
      17              : namespace engine {
      18              : namespace duckdb {
      19              : namespace udf {
      20              : namespace {
      21              : 
      22              : class DatetimeMacrosTest : public ::testing::Test {
      23              :  protected:
      24           22 :   void SetUp() override {
      25           22 :     ASSERT_EQ(::duckdb_open(nullptr, &db_), ::DuckDBSuccess);
      26           22 :     ASSERT_EQ(::duckdb_connect(db_, &conn_), ::DuckDBSuccess);
      27           22 :     absl::Status reg = RegisterAll(conn_);
      28           44 :     ASSERT_TRUE(reg.ok()) << reg;
      29           22 :   }
      30              : 
      31           22 :   void TearDown() override {
      32           22 :     if (conn_ != nullptr) ::duckdb_disconnect(&conn_);
      33           22 :     if (db_ != nullptr) ::duckdb_close(&db_);
      34           22 :   }
      35              : 
      36           15 :   int64_t RunInt64(const std::string& sql) {
      37           15 :     ::duckdb_result result;
      38           15 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      39           30 :     EXPECT_EQ(rc, ::DuckDBSuccess) << "DuckDB rejected: "
      40           30 :                                    << (::duckdb_result_error(&result) == nullptr
      41           30 :                                            ? "(no error)"
      42           30 :                                            : ::duckdb_result_error(&result))
      43           30 :                                    << " (sql=" << sql << ")";
      44           15 :     int64_t v = ::duckdb_value_int64(&result, 0, 0);
      45           15 :     ::duckdb_destroy_result(&result);
      46           15 :     return v;
      47           15 :   }
      48              : 
      49           10 :   bool RunBool(const std::string& sql) {
      50           10 :     ::duckdb_result result;
      51           10 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      52           20 :     EXPECT_EQ(rc, ::DuckDBSuccess) << "DuckDB rejected: " << sql;
      53           10 :     bool v = ::duckdb_value_boolean(&result, 0, 0);
      54           10 :     ::duckdb_destroy_result(&result);
      55           10 :     return v;
      56           10 :   }
      57              : 
      58              :   // Statement with no interesting result (e.g. SET TimeZone).
      59            1 :   void RunOk(const std::string& sql) {
      60            1 :     ::duckdb_result result;
      61            1 :     auto rc = ::duckdb_query(conn_, sql.c_str(), &result);
      62            2 :     EXPECT_EQ(rc, ::DuckDBSuccess) << "DuckDB rejected: " << sql;
      63            1 :     ::duckdb_destroy_result(&result);
      64            1 :   }
      65              : 
      66              :   ::duckdb_database db_ = nullptr;
      67              :   ::duckdb_connection conn_ = nullptr;
      68              : };
      69              : 
      70              : // 2024-01-01 00:00:00 UTC -> 1704067200 seconds since epoch.
      71              : constexpr int64_t kSec2024_01_01 = 1704067200;
      72              : constexpr int64_t kMs2024_01_01 = 1704067200LL * 1000;
      73              : constexpr int64_t kUs2024_01_01 = 1704067200LL * 1000 * 1000;
      74              : 
      75              : // --- bq_unix_seconds ---------------------------------------------
      76              : 
      77            1 : TEST_F(DatetimeMacrosTest, UnixSecondsWholeSecond) {
      78            1 :   EXPECT_EQ(
      79            1 :       RunInt64("SELECT bq_unix_seconds(TIMESTAMPTZ '2024-01-01 00:00:00+00')"),
      80            1 :       kSec2024_01_01);
      81            1 : }
      82              : 
      83            1 : TEST_F(DatetimeMacrosTest, UnixSecondsTruncatesSubsecond) {
      84              :   // Edge case pinned: BigQuery UNIX_SECONDS truncates higher
      85              :   // precision. A regression that switched the CAST to a rounding
      86              :   // form would surface here as 1704067201 (rounding .999 up to
      87              :   // the next second) instead of 1704067200.
      88            1 :   EXPECT_EQ(
      89            1 :       RunInt64(
      90            1 :           "SELECT bq_unix_seconds(TIMESTAMPTZ '2024-01-01 00:00:00.999+00')"),
      91            1 :       kSec2024_01_01);
      92            1 : }
      93              : 
      94            1 : TEST_F(DatetimeMacrosTest, UnixSecondsNullPropagation) {
      95            1 :   EXPECT_TRUE(RunBool("SELECT bq_unix_seconds(NULL::TIMESTAMPTZ) IS NULL"));
      96            1 : }
      97              : 
      98              : // --- bq_unix_millis ----------------------------------------------
      99              : 
     100            1 : TEST_F(DatetimeMacrosTest, UnixMillisWholeSecond) {
     101            1 :   EXPECT_EQ(
     102            1 :       RunInt64("SELECT bq_unix_millis(TIMESTAMPTZ '2024-01-01 00:00:00+00')"),
     103            1 :       kMs2024_01_01);
     104            1 : }
     105              : 
     106            1 : TEST_F(DatetimeMacrosTest, UnixMillisSubsecond) {
     107              :   // Fractional milliseconds. 2024-01-01 00:00:00.5 -> +500 ms.
     108            1 :   EXPECT_EQ(
     109            1 :       RunInt64("SELECT bq_unix_millis(TIMESTAMPTZ '2024-01-01 00:00:00.5+00')"),
     110            1 :       kMs2024_01_01 + 500);
     111            1 : }
     112              : 
     113            1 : TEST_F(DatetimeMacrosTest, UnixMillisNullPropagation) {
     114            1 :   EXPECT_TRUE(RunBool("SELECT bq_unix_millis(NULL::TIMESTAMPTZ) IS NULL"));
     115            1 : }
     116              : 
     117              : // --- bq_unix_micros ----------------------------------------------
     118              : 
     119            1 : TEST_F(DatetimeMacrosTest, UnixMicrosWholeSecond) {
     120            1 :   EXPECT_EQ(
     121            1 :       RunInt64("SELECT bq_unix_micros(TIMESTAMPTZ '2024-01-01 00:00:00+00')"),
     122            1 :       kUs2024_01_01);
     123            1 : }
     124              : 
     125            1 : TEST_F(DatetimeMacrosTest, UnixMicrosSubsecond) {
     126              :   // 2024-01-01 00:00:00.123456 -> +123456 microseconds.
     127            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_micros(TIMESTAMPTZ "
     128            1 :                      "'2024-01-01 00:00:00.123456+00')"),
     129            1 :             kUs2024_01_01 + 123456);
     130            1 : }
     131              : 
     132            1 : TEST_F(DatetimeMacrosTest, UnixMicrosNullPropagation) {
     133            1 :   EXPECT_TRUE(RunBool("SELECT bq_unix_micros(NULL::TIMESTAMPTZ) IS NULL"));
     134            1 : }
     135              : 
     136              : // --- bq_unix_date ------------------------------------------------
     137              : 
     138            1 : TEST_F(DatetimeMacrosTest, UnixDateEpochIsZero) {
     139            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_date(DATE '1970-01-01')"), 0);
     140            1 : }
     141              : 
     142            1 : TEST_F(DatetimeMacrosTest, UnixDatePreEpochIsNegative) {
     143              :   // Edge case pinned: pre-1970 dates return negative day counts.
     144              :   // BigQuery documents this explicitly. A regression that wrapped
     145              :   // the result in `ABS(...)` or `GREATEST(..., 0)` would surface
     146              :   // here.
     147            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_date(DATE '1969-12-31')"), -1);
     148            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_date(DATE '1969-12-01')"), -31);
     149            1 : }
     150              : 
     151            1 : TEST_F(DatetimeMacrosTest, UnixDateFutureDate) {
     152              :   // 2024-01-01 is 19723 days after 1970-01-01.
     153              :   // (54 years + 13 leap days = 19723 days.)
     154            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_date(DATE '2024-01-01')"), 19723);
     155            1 : }
     156              : 
     157            1 : TEST_F(DatetimeMacrosTest, UnixDateNullPropagation) {
     158            1 :   EXPECT_TRUE(RunBool("SELECT bq_unix_date(NULL::DATE) IS NULL"));
     159            1 : }
     160              : 
     161            1 : TEST_F(DatetimeMacrosTest, DateAddDay) {
     162            1 :   EXPECT_TRUE(RunBool(
     163            1 :       "SELECT bq_date_add(DATE '2020-09-22', 1, 3) = DATE '2020-09-23'"));
     164            1 : }
     165              : 
     166            1 : TEST_F(DatetimeMacrosTest, DateAddMonthEndSnap) {
     167            1 :   EXPECT_TRUE(RunBool(
     168            1 :       "SELECT bq_date_add(DATE '2024-01-31', 1, 2) = DATE '2024-02-29'"));
     169            1 : }
     170              : 
     171            1 : TEST_F(DatetimeMacrosTest, ExtractYear) {
     172            1 :   EXPECT_EQ(RunInt64("SELECT bq_extract(1, DATE '2020-06-15')"), 2020);
     173            1 : }
     174              : 
     175              : // --- bq_timestamp_add / bq_timestamp_sub -------------------------
     176              : 
     177            1 : TEST_F(DatetimeMacrosTest, TimestampAddSevenDaysLiteral) {
     178              :   // Same shape the conformance fixture exercises: n=7, part=DAY.
     179            1 :   EXPECT_TRUE(RunBool(
     180            1 :       "SELECT CAST(bq_timestamp_add(TIMESTAMPTZ '2024-03-09 12:00:00+00', "
     181            1 :       "7, 3) AS VARCHAR) = '2024-03-16 12:00:00+00'"));
     182            1 :   EXPECT_TRUE(RunBool(
     183            1 :       "SELECT typeof(bq_timestamp_add(TIMESTAMPTZ '2024-03-09 12:00:00+00', "
     184            1 :       "7, 3)) = 'TIMESTAMP WITH TIME ZONE'"));
     185            1 : }
     186              : 
     187            1 : TEST_F(DatetimeMacrosTest, TimestampAddDayIsExactly24hAcrossDst) {
     188              :   // Edge case pinned: BigQuery TIMESTAMP_ADD(t, INTERVAL 1 DAY) adds
     189              :   // exactly 86400 seconds. DuckDB's calendar-day interval on
     190              :   // TIMESTAMPTZ would add "one local day" (23h across the 2024-03-10
     191              :   // US spring-forward) in a non-UTC session zone; the macro's
     192              :   // to_microseconds arithmetic must not.
     193            1 :   RunOk("SET TimeZone='America/Los_Angeles'");
     194            1 :   EXPECT_EQ(RunInt64("SELECT epoch_us(bq_timestamp_add("
     195            1 :                      "TIMESTAMPTZ '2024-03-09 12:00:00-08', 1, 3)) - "
     196            1 :                      "epoch_us(TIMESTAMPTZ '2024-03-09 12:00:00-08')"),
     197            1 :             int64_t{86400} * 1000 * 1000);
     198            1 : }
     199              : 
     200            1 : TEST_F(DatetimeMacrosTest, TimestampAddMicrosecondGranularity) {
     201            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_micros(bq_timestamp_add("
     202            1 :                      "TIMESTAMPTZ '2024-01-01 00:00:00+00', 123456, 11))"),
     203            1 :             kUs2024_01_01 + 123456);
     204            1 : }
     205              : 
     206            1 : TEST_F(DatetimeMacrosTest, TimestampAddNegativeAmountSubtracts) {
     207            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_seconds(bq_timestamp_add("
     208            1 :                      "TIMESTAMPTZ '2024-01-01 00:00:00+00', -2, 7))"),
     209            1 :             kSec2024_01_01 - 2 * 3600);
     210            1 : }
     211              : 
     212            1 : TEST_F(DatetimeMacrosTest, TimestampSubMirrorsAdd) {
     213            1 :   EXPECT_EQ(RunInt64("SELECT bq_unix_seconds(bq_timestamp_sub("
     214            1 :                      "TIMESTAMPTZ '2024-01-01 00:00:00+00', 90, 8))"),
     215            1 :             kSec2024_01_01 - 90 * 60);
     216            1 : }
     217              : 
     218            1 : TEST_F(DatetimeMacrosTest, TimestampAddNullPropagation) {
     219            1 :   EXPECT_TRUE(
     220            1 :       RunBool("SELECT bq_timestamp_add(NULL::TIMESTAMPTZ, 1, 3) IS NULL"));
     221            1 :   EXPECT_TRUE(
     222            1 :       RunBool("SELECT bq_timestamp_add(TIMESTAMPTZ '2024-01-01 00:00:00+00', "
     223            1 :               "NULL, 3) IS NULL"));
     224            1 : }
     225              : 
     226              : }  // namespace
     227              : }  // namespace udf
     228              : }  // namespace duckdb
     229              : }  // namespace engine
     230              : }  // namespace backend
     231              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1