This is an automated email from the ASF dual-hosted git repository.

kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 57139b0db2 GH-50136: [C++][Gandiva] Enhance CHR to work with unicode 
(#50137)
57139b0db2 is described below

commit 57139b0db2c6f85bf2cdd9525107398f646e5c97
Author: Logan Riggs <[email protected]>
AuthorDate: Thu Aug 13 14:28:50 2026 -0700

    GH-50136: [C++][Gandiva] Enhance CHR to work with unicode (#50137)
    
    ### Rationale for this change
    `CHR(n)` only worked for ASCII (0–127). Values ≥ 128 emitted a single raw 
byte
    (invalid UTF‑8), causing "Error during planning". Goal: emit the proper
    multi‑byte **UTF‑8 encoding** of the Unicode code point, consistent with
    PostgreSQL/Snowflake.
    
    ### What changes are included in this PR?
    #### Arrow (C++ / Gandiva)
    
    | File | Change |
    |------|--------|
    | `cpp/src/gandiva/precompiled/string_ops.cc` | `chr_int64` rewritten to 
UTF‑8‑encode the code point (1–4 bytes) and error on invalid input (negative, > 
0x10FFFF, surrogate range 0xD800–0xDFFF). `chr_int32` now delegates to it. |
    | `cpp/src/gandiva/precompiled/string_ops_test.cc` | `TestChrBigInt` 
rewritten for UTF‑8 semantics: every byte‑length boundary (1/2/3/4‑byte, 
low+high), í/€/日/😀, and the three invalid‑input error cases. |
    
    ### Are these changes tested?
    Yes, unit tests.
    
    ### Are there any user-facing changes?
    Yes, the CHR gandiva function now supports unicode characters.
    
    * GitHub Issue: #50136
    
    Authored-by: [email protected] <[email protected]>
    Signed-off-by: Sutou Kouhei <[email protected]>
---
 cpp/src/gandiva/precompiled/string_ops.cc      |  67 ++++++++++-----
 cpp/src/gandiva/precompiled/string_ops_test.cc | 110 ++++++++++++++++---------
 cpp/src/gandiva/tests/projector_test.cc        |  44 ++++++++--
 3 files changed, 156 insertions(+), 65 deletions(-)

diff --git a/cpp/src/gandiva/precompiled/string_ops.cc 
b/cpp/src/gandiva/precompiled/string_ops.cc
index 2bd8e4076b..b7c6b518b4 100644
--- a/cpp/src/gandiva/precompiled/string_ops.cc
+++ b/cpp/src/gandiva/precompiled/string_ops.cc
@@ -1417,29 +1417,32 @@ gdv_int32 ascii_utf8(const char* data, gdv_int32 
data_len) {
   return static_cast<gdv_int32>(static_cast<signed char>(data[0]));
 }
 
-// Returns the ASCII character having the binary equivalent to A.
-// If A is larger than 256 the result is equivalent to chr(A % 256).
+// Returns the UTF-8 encoding of the Unicode code point A.
+// Raises an error if A is not a valid code point, i.e. it is negative, greater
+// than 0x10FFFF, or falls within the UTF-16 surrogate range 0xD800-0xDFFF.
 FORCE_INLINE
-const char* chr_int32(gdv_int64 context, gdv_int32 in, gdv_int32* out_len) {
-  in = in % 256;
-  *out_len = 1;
-
-  char* ret = reinterpret_cast<char*>(gdv_fn_context_arena_malloc(context, 
*out_len));
-  if (ret == nullptr) {
-    gdv_fn_context_set_error_msg(context, "Could not allocate memory for 
output string");
+const char* chr_int64(gdv_int64 context, gdv_int64 in, gdv_int32* out_len) {
+  if (in < 0 || in > 0x10FFFF || (in >= 0xD800 && in <= 0xDFFF)) {
+    char err_msg[128];
+    snprintf(err_msg, sizeof(err_msg),
+             "Input %" PRId64
+             " is not a valid Unicode code point in the range 0 to 1114111, 
excluding "
+             "the surrogate range 0xD800–0xDFFF",
+             in);
+    gdv_fn_context_set_error_msg(context, err_msg);
     *out_len = 0;
     return "";
   }
-  ret[0] = char(in);
-  return ret;
-}
 
-// Returns the ASCII character having the binary equivalent to A.
-// If A is larger than 256 the result is equivalent to chr(A % 256).
-FORCE_INLINE
-const char* chr_int64(gdv_int64 context, gdv_int64 in, gdv_int32* out_len) {
-  in = in % 256;
-  *out_len = 1;
+  if (in <= 0x7F) {
+    *out_len = 1;
+  } else if (in <= 0x7FF) {
+    *out_len = 2;
+  } else if (in <= 0xFFFF) {
+    *out_len = 3;
+  } else {
+    *out_len = 4;
+  }
 
   char* ret = reinterpret_cast<char*>(gdv_fn_context_arena_malloc(context, 
*out_len));
   if (ret == nullptr) {
@@ -1447,10 +1450,36 @@ const char* chr_int64(gdv_int64 context, gdv_int64 in, 
gdv_int32* out_len) {
     *out_len = 0;
     return "";
   }
-  ret[0] = char(in);
+
+  switch (*out_len) {
+    case 1:
+      ret[0] = static_cast<char>(in);
+      break;
+    case 2:
+      ret[0] = static_cast<char>(0xC0 | (in >> 6));
+      ret[1] = static_cast<char>(0x80 | (in & 0x3F));
+      break;
+    case 3:
+      ret[0] = static_cast<char>(0xE0 | (in >> 12));
+      ret[1] = static_cast<char>(0x80 | ((in >> 6) & 0x3F));
+      ret[2] = static_cast<char>(0x80 | (in & 0x3F));
+      break;
+    case 4:
+      ret[0] = static_cast<char>(0xF0 | (in >> 18));
+      ret[1] = static_cast<char>(0x80 | ((in >> 12) & 0x3F));
+      ret[2] = static_cast<char>(0x80 | ((in >> 6) & 0x3F));
+      ret[3] = static_cast<char>(0x80 | (in & 0x3F));
+      break;
+  }
   return ret;
 }
 
+// Returns the UTF-8 encoding of the Unicode code point A. See chr_int64.
+FORCE_INLINE
+const char* chr_int32(gdv_int64 context, gdv_int32 in, gdv_int32* out_len) {
+  return chr_int64(context, in, out_len);
+}
+
 FORCE_INLINE
 const char* convert_fromUTF8_binary(gdv_int64 context, const char* bin_in, 
gdv_int32 len,
                                     gdv_int32* out_len) {
diff --git a/cpp/src/gandiva/precompiled/string_ops_test.cc 
b/cpp/src/gandiva/precompiled/string_ops_test.cc
index 25dbb339fe..3faeb31623 100644
--- a/cpp/src/gandiva/precompiled/string_ops_test.cc
+++ b/cpp/src/gandiva/precompiled/string_ops_test.cc
@@ -58,11 +58,12 @@ TEST(TestStringOps, TestAscii) {
 }
 
 TEST(TestStringOps, TestChrBigInt) {
-  // CHR
+  // CHR returns the UTF-8 encoding of the given Unicode code point.
   gandiva::ExecutionContext ctx;
   uint64_t ctx_ptr = reinterpret_cast<gdv_int64>(&ctx);
   int32_t out_len = 0;
 
+  // 1-byte ASCII code points.
   auto out = chr_int32(ctx_ptr, 88, &out_len);
   EXPECT_EQ(std::string(out, out_len), "X");
 
@@ -72,62 +73,89 @@ TEST(TestStringOps, TestChrBigInt) {
   out = chr_int32(ctx_ptr, 49, &out_len);
   EXPECT_EQ(std::string(out, out_len), "1");
 
-  out = chr_int64(ctx_ptr, 84, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "T");
+  out = chr_int32(ctx_ptr, 33, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "!");
 
-  out = chr_int32(ctx_ptr, 340, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "T");
+  out = chr_int64(ctx_ptr, 0, &out_len);
+  EXPECT_EQ(std::string(out, out_len), std::string("\0", 1));
 
-  out = chr_int64(ctx_ptr, 256, &out_len);
-  EXPECT_EQ(std::strcmp(out, "\0"), 0);
+  // BACKSPACE
+  out = chr_int64(ctx_ptr, 8, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\b");
 
-  out = chr_int32(ctx_ptr, 33, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "!");
+  // ESCAPE (ESC)
+  out = chr_int64(ctx_ptr, 27, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\x1B");
 
-  out = chr_int64(ctx_ptr, 46, &out_len);
-  EXPECT_EQ(std::string(out, out_len), ".");
+  // Highest 1-byte code point (U+007F, DELETE).
+  out = chr_int32(ctx_ptr, 0x7F, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\x7F");
 
-  out = chr_int32(ctx_ptr, 63, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "?");
+  // 2-byte code points.
+  // Lowest 2-byte code point (U+0080).
+  out = chr_int32(ctx_ptr, 0x80, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xC2\x80");
 
-  out = chr_int64(ctx_ptr, 0, &out_len);
-  EXPECT_EQ(std::strcmp(out, "\0"), 0);
+  // í (U+00ED)
+  out = chr_int32(ctx_ptr, 237, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xC3\xAD");
 
-  out = chr_int32(ctx_ptr, -158, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "b");
+  // Highest 2-byte code point (U+07FF).
+  out = chr_int64(ctx_ptr, 0x7FF, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xDF\xBF");
 
-  out = chr_int64(ctx_ptr, -5, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\xFB");
+  // 3-byte code points.
+  // Lowest 3-byte code point (U+0800).
+  out = chr_int32(ctx_ptr, 0x800, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xE0\xA0\x80");
 
-  out = chr_int32(ctx_ptr, -340, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\xAC");
+  // € (U+20AC)
+  out = chr_int32(ctx_ptr, 8364, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xE2\x82\xAC");
 
-  out = chr_int64(ctx_ptr, -66, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\xBE");
+  // 日 (U+65E5)
+  out = chr_int64(ctx_ptr, 26085, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xE6\x97\xA5");
 
-  // €
-  out = chr_int32(ctx_ptr, 128, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\x80");
+  // Highest 3-byte code point (U+FFFF).
+  out = chr_int32(ctx_ptr, 0xFFFF, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xEF\xBF\xBF");
 
-  // œ
-  out = chr_int64(ctx_ptr, 156, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\x9C");
+  // 4-byte code points.
+  // Lowest 4-byte code point (U+10000).
+  out = chr_int64(ctx_ptr, 0x10000, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xF0\x90\x80\x80");
 
-  // ÿ
-  out = chr_int32(ctx_ptr, 255, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\xFF");
+  // 😀 (U+1F600)
+  out = chr_int64(ctx_ptr, 0x1F600, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xF0\x9F\x98\x80");
 
-  // BACKSPACE
-  out = chr_int64(ctx_ptr, 8, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\b");
+  // Highest valid code point (U+10FFFF).
+  out = chr_int64(ctx_ptr, 0x10FFFF, &out_len);
+  EXPECT_EQ(std::string(out, out_len), "\xF4\x8F\xBF\xBF");
 
-  // DEVICE CONTROL 3 (DC3)
-  out = chr_int32(ctx_ptr, 19, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\x13");
+  EXPECT_FALSE(ctx.has_error());
 
-  // ESCAPE (ESC)
-  out = chr_int64(ctx_ptr, 27, &out_len);
-  EXPECT_EQ(std::string(out, out_len), "\x1B");
+  // Invalid code points raise an error that includes the offending value.
+  chr_int64(ctx_ptr, -1, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.get_error().find("not a valid Unicode code point") != 
std::string::npos)
+      << ctx.get_error();
+  EXPECT_TRUE(ctx.get_error().find("-1") != std::string::npos) << 
ctx.get_error();
+  ctx.Reset();
+
+  chr_int64(ctx_ptr, 0x110000, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.get_error().find("not a valid Unicode code point") != 
std::string::npos)
+      << ctx.get_error();
+  ctx.Reset();
+
+  // UTF-16 surrogate range is not a valid code point.
+  chr_int32(ctx_ptr, 0xD800, &out_len);
+  EXPECT_EQ(out_len, 0);
+  EXPECT_TRUE(ctx.get_error().find("not a valid Unicode code point") != 
std::string::npos)
+      << ctx.get_error();
+  ctx.Reset();
 }
 
 TEST(TestStringOps, TestBeginsEnds) {
diff --git a/cpp/src/gandiva/tests/projector_test.cc 
b/cpp/src/gandiva/tests/projector_test.cc
index facd1084ca..27beb8595c 100644
--- a/cpp/src/gandiva/tests/projector_test.cc
+++ b/cpp/src/gandiva/tests/projector_test.cc
@@ -1325,13 +1325,16 @@ TEST_F(TestProjector, TestChr) {
   auto status = Projector::Make(schema, {chr_expr}, TestConfiguration(), 
&projector);
   EXPECT_TRUE(status.ok()) << status.message();
 
-  // Create a row-batch with some sample data
+  // Create a row-batch with code points spanning 1- to 4-byte UTF-8 encodings.
+  // 65 -> "A", 237 -> "í" (U+00ED), 8364 -> "€" (U+20AC), 26085 -> "日" 
(U+65E5),
+  // 128512 -> "😀" (U+1F600).
   int num_records = 5;
   auto array0 =
-      MakeArrowArrayInt64({65, 84, 255, 340, -5}, {true, true, true, true, 
true});
-  // expected output
-  auto exp_chr =
-      MakeArrowArrayUtf8({"A", "T", "\xFF", "T", "\xFB"}, {true, true, true, 
true, true});
+      MakeArrowArrayInt64({65, 237, 8364, 26085, 128512}, {true, true, true, 
true, true});
+  // expected UTF-8 output
+  auto exp_chr = MakeArrowArrayUtf8(
+      {"A", "\xC3\xAD", "\xE2\x82\xAC", "\xE6\x97\xA5", "\xF0\x9F\x98\x80"},
+      {true, true, true, true, true});
 
   // prepare input record batch
   auto in_batch = arrow::RecordBatch::Make(schema, num_records, {array0});
@@ -1345,6 +1348,37 @@ TEST_F(TestProjector, TestChr) {
   EXPECT_ARROW_ARRAY_EQUALS(exp_chr, outputs.at(0));
 }
 
+TEST_F(TestProjector, TestChrInvalidCodePoint) {
+  // schema for input fields
+  auto field0 = field("f0", int64());
+  auto schema = arrow::schema({field0});
+
+  // output fields
+  auto field_chr = field("chr", arrow::utf8());
+
+  // Build expression
+  auto chr_expr = TreeExprBuilder::MakeExpression("chr", {field0}, field_chr);
+
+  std::shared_ptr<Projector> projector;
+  auto status = Projector::Make(schema, {chr_expr}, TestConfiguration(), 
&projector);
+  EXPECT_TRUE(status.ok()) << status.message();
+
+  // A code point outside the valid Unicode range (here, a negative value) must
+  // fail evaluation rather than wrap around.
+  int num_records = 1;
+  auto array0 = MakeArrowArrayInt64({-5}, {true});
+  auto in_batch = arrow::RecordBatch::Make(schema, num_records, {array0});
+
+  // Evaluate expression
+  arrow::ArrayVector outputs;
+  status = projector->Evaluate(*in_batch, pool_, &outputs);
+  EXPECT_FALSE(status.ok());
+  EXPECT_NE(status.message().find("not a valid Unicode code point"), 
std::string::npos)
+      << status.message();
+  // The message should include the offending value for debuggability.
+  EXPECT_NE(status.message().find("-5"), std::string::npos) << 
status.message();
+}
+
 TEST_F(TestProjector, TestBase64) {
   // schema for input fields
   auto field0 = field("f0", arrow::binary());

Reply via email to