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
|