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

            Line data    Source code
       1              : // Unit tests for the string-functions semantic-executor helpers.
       2              : 
       3              : #include "backend/engine/semantic/functions/string_funcs.h"
       4              : 
       5              : #include <vector>
       6              : 
       7              : #include "absl/status/status.h"
       8              : #include "backend/engine/semantic/error.h"
       9              : #include "backend/engine/semantic/value.h"
      10              : #include "googlesql/public/value.h"
      11              : #include "gtest/gtest.h"
      12              : 
      13              : namespace bigquery_emulator {
      14              : namespace backend {
      15              : namespace engine {
      16              : namespace semantic {
      17              : namespace functions {
      18              : namespace {
      19              : 
      20              : // Reference SOUNDEX values from
      21              : // https://en.wikipedia.org/wiki/Soundex and BigQuery's
      22              : // `string_functions` documentation (`SOUNDEX('Ashcraft') ->
      23              : // A261`). Holding these stable is the contract of the row in
      24              : // `functions.yaml`.
      25            1 : TEST(SoundexTest, ClassicReferenceValues) {
      26            1 :   Value robert = Value::String("Robert");
      27            1 :   EXPECT_EQ(Soundex({robert})->string_value(), "R163");
      28            1 :   Value rupert = Value::String("Rupert");
      29            1 :   EXPECT_EQ(Soundex({rupert})->string_value(), "R163");
      30            1 :   Value rubin = Value::String("Rubin");
      31            1 :   EXPECT_EQ(Soundex({rubin})->string_value(), "R150");
      32            1 :   Value ashcraft = Value::String("Ashcraft");
      33            1 :   EXPECT_EQ(Soundex({ashcraft})->string_value(), "A261");
      34            1 :   Value tymczak = Value::String("Tymczak");
      35            1 :   EXPECT_EQ(Soundex({tymczak})->string_value(), "T522");
      36            1 :   Value pfister = Value::String("Pfister");
      37            1 :   EXPECT_EQ(Soundex({pfister})->string_value(), "P236");
      38            1 :   Value honeman = Value::String("Honeyman");
      39            1 :   EXPECT_EQ(Soundex({honeman})->string_value(), "H555");
      40            1 : }
      41              : 
      42            1 : TEST(SoundexTest, EmptyStringRoundTrips) {
      43            1 :   Value arg = Value::String("");
      44            1 :   auto v = Soundex({arg});
      45            2 :   ASSERT_TRUE(v.ok()) << v.status();
      46            1 :   EXPECT_EQ(v->string_value(), "");
      47            1 : }
      48              : 
      49            1 : TEST(SoundexTest, NullPropagates) {
      50            1 :   Value arg = Value::NullString();
      51            1 :   auto v = Soundex({arg});
      52            2 :   ASSERT_TRUE(v.ok()) << v.status();
      53            1 :   EXPECT_TRUE(v->is_null());
      54            1 : }
      55              : 
      56            1 : TEST(SoundexTest, ShortStringPadsWithZeros) {
      57            1 :   Value a = Value::String("A");
      58            1 :   EXPECT_EQ(Soundex({a})->string_value(), "A000");
      59            1 :   Value hi = Value::String("Hi");
      60            1 :   EXPECT_EQ(Soundex({hi})->string_value(), "H000");
      61            1 : }
      62              : 
      63            1 : TEST(SoundexTest, NonLetterPrefixSkipped) {
      64              :   // Leading punctuation/digits do not anchor SOUNDEX -- the
      65              :   // first SOUNDEX-eligible letter does.
      66            1 :   Value arg = Value::String("123Robert");
      67            1 :   EXPECT_EQ(Soundex({arg})->string_value(), "R163");
      68            1 : }
      69              : 
      70            1 : TEST(SoundexTest, AllLettersWithoutCodeReturnsLetterAndZeros) {
      71            1 :   Value arg = Value::String("Aeiouy");
      72            1 :   EXPECT_EQ(Soundex({arg})->string_value(), "A000");
      73            1 : }
      74              : 
      75            1 : TEST(SoundexTest, NonStringRejected) {
      76            1 :   Value arg = Value::Int64(7);
      77            1 :   auto v = Soundex({arg});
      78            1 :   ASSERT_FALSE(v.ok());
      79            1 :   EXPECT_EQ(GetSemanticErrorReason(v.status()),
      80            1 :             SemanticErrorReason::kInvalidArgument);
      81            1 : }
      82              : 
      83            1 : TEST(InstrTest, BasicTwoArg) {
      84            1 :   Value haystack = Value::String("hello world");
      85            1 :   Value needle = Value::String("world");
      86            1 :   auto v = Instr({haystack, needle});
      87            2 :   ASSERT_TRUE(v.ok()) << v.status();
      88            1 :   EXPECT_EQ(v->int64_value(), 7);
      89            1 : }
      90              : 
      91            1 : TEST(InstrTest, NotFoundReturnsZero) {
      92            1 :   Value haystack = Value::String("hello");
      93            1 :   Value needle = Value::String("xyz");
      94            1 :   auto v = Instr({haystack, needle});
      95            2 :   ASSERT_TRUE(v.ok()) << v.status();
      96            1 :   EXPECT_EQ(v->int64_value(), 0);
      97            1 : }
      98              : 
      99            1 : TEST(InstrTest, PositionStartsSearch) {
     100            1 :   Value haystack = Value::String("ababab");
     101            1 :   Value needle = Value::String("ab");
     102            1 :   Value position = Value::Int64(2);
     103            1 :   auto v = Instr({haystack, needle, position});
     104            2 :   ASSERT_TRUE(v.ok()) << v.status();
     105            1 :   EXPECT_EQ(v->int64_value(), 3);
     106            1 : }
     107              : 
     108            1 : TEST(InstrTest, OccurrencePicksNthMatch) {
     109            1 :   Value haystack = Value::String("ababab");
     110            1 :   Value needle = Value::String("ab");
     111            1 :   Value position = Value::Int64(1);
     112            1 :   Value occurrence = Value::Int64(2);
     113            1 :   auto v = Instr({haystack, needle, position, occurrence});
     114            2 :   ASSERT_TRUE(v.ok()) << v.status();
     115            1 :   EXPECT_EQ(v->int64_value(), 3);
     116            1 : }
     117              : 
     118            1 : TEST(InstrTest, ThirdOccurrence) {
     119            1 :   Value haystack = Value::String("ababab");
     120            1 :   Value needle = Value::String("ab");
     121            1 :   Value position = Value::Int64(1);
     122            1 :   Value occurrence = Value::Int64(3);
     123            1 :   auto v = Instr({haystack, needle, position, occurrence});
     124            2 :   ASSERT_TRUE(v.ok()) << v.status();
     125            1 :   EXPECT_EQ(v->int64_value(), 5);
     126            1 : }
     127              : 
     128            1 : TEST(InstrTest, NegativePositionSearchesFromEnd) {
     129              :   // BigQuery: INSTR('ababab', 'ab', -1) returns the last
     130              :   // occurrence's 1-based position (5).
     131            1 :   Value haystack = Value::String("ababab");
     132            1 :   Value needle = Value::String("ab");
     133            1 :   Value position = Value::Int64(-1);
     134            1 :   auto v = Instr({haystack, needle, position});
     135            2 :   ASSERT_TRUE(v.ok()) << v.status();
     136            1 :   EXPECT_EQ(v->int64_value(), 5);
     137            1 : }
     138              : 
     139            1 : TEST(InstrTest, NegativePositionWithOccurrence) {
     140            1 :   Value haystack = Value::String("ababab");
     141            1 :   Value needle = Value::String("ab");
     142            1 :   Value position = Value::Int64(-1);
     143            1 :   Value occurrence = Value::Int64(2);
     144            1 :   auto v = Instr({haystack, needle, position, occurrence});
     145            2 :   ASSERT_TRUE(v.ok()) << v.status();
     146            1 :   EXPECT_EQ(v->int64_value(), 3);
     147            1 : }
     148              : 
     149            1 : TEST(InstrTest, EmptySubvalueReturnsPosition) {
     150            1 :   Value haystack = Value::String("hello");
     151            1 :   Value needle = Value::String("");
     152            1 :   auto v = Instr({haystack, needle});
     153            2 :   ASSERT_TRUE(v.ok()) << v.status();
     154            1 :   EXPECT_EQ(v->int64_value(), 1);
     155            1 : }
     156              : 
     157            1 : TEST(InstrTest, NullPropagates) {
     158            1 :   Value haystack = Value::String("hi");
     159            1 :   Value needle = Value::NullString();
     160            1 :   auto v = Instr({haystack, needle});
     161            2 :   ASSERT_TRUE(v.ok()) << v.status();
     162            1 :   EXPECT_TRUE(v->is_null());
     163            1 : }
     164              : 
     165            1 : TEST(InstrTest, ZeroPositionRejected) {
     166            1 :   Value haystack = Value::String("hi");
     167            1 :   Value needle = Value::String("h");
     168            1 :   Value position = Value::Int64(0);
     169            1 :   auto v = Instr({haystack, needle, position});
     170            1 :   ASSERT_FALSE(v.ok());
     171            1 :   EXPECT_EQ(GetSemanticErrorReason(v.status()),
     172            1 :             SemanticErrorReason::kInvalidArgument);
     173            1 : }
     174              : 
     175            1 : TEST(InstrTest, NonPositiveOccurrenceRejected) {
     176            1 :   Value haystack = Value::String("hi");
     177            1 :   Value needle = Value::String("h");
     178            1 :   Value position = Value::Int64(1);
     179            1 :   Value occurrence = Value::Int64(0);
     180            1 :   auto v = Instr({haystack, needle, position, occurrence});
     181            1 :   ASSERT_FALSE(v.ok());
     182            1 :   EXPECT_EQ(GetSemanticErrorReason(v.status()),
     183            1 :             SemanticErrorReason::kInvalidArgument);
     184            1 : }
     185              : 
     186            1 : TEST(ContainsSubstrTest, CaseInsensitiveStringMatch) {
     187            1 :   Value haystack = Value::String("the blue house");
     188            1 :   Value needle = Value::String("Blue house");
     189            1 :   auto v = ContainsSubstr({haystack, needle});
     190            2 :   ASSERT_TRUE(v.ok()) << v.status();
     191            1 :   EXPECT_TRUE(v->bool_value());
     192            1 : }
     193              : 
     194            1 : TEST(ContainsSubstrTest, UnicodeNormalizationMatch) {
     195            1 :   Value haystack = Value::String("\u2168");
     196            1 :   Value needle = Value::String("IX");
     197            1 :   auto v = ContainsSubstr({haystack, needle});
     198            2 :   ASSERT_TRUE(v.ok()) << v.status();
     199            1 :   EXPECT_TRUE(v->bool_value());
     200            1 : }
     201              : 
     202            1 : TEST(ContainsSubstrTest, NullSearchValueRejected) {
     203            1 :   Value haystack = Value::String("hello");
     204            1 :   Value needle = Value::NullString();
     205            1 :   auto v = ContainsSubstr({haystack, needle});
     206            1 :   ASSERT_FALSE(v.ok());
     207            1 : }
     208              : 
     209              : }  // namespace
     210              : }  // namespace functions
     211              : }  // namespace semantic
     212              : }  // namespace engine
     213              : }  // namespace backend
     214              : }  // namespace bigquery_emulator
        

Generated by: LCOV version 2.0-1