You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2026/08/07 16:48:33 UTC

(iceberg-go) branch main updated: fix(table): advance string upper bounds across surrogate gap (#1645)

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

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 847d9de7 fix(table): advance string upper bounds across surrogate gap (#1645)
847d9de7 is described below

commit 847d9de7b174d2001ade04d522c756189f4a6ed1
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Fri Aug 7 18:48:28 2026 +0200

    fix(table): advance string upper bounds across surrogate gap (#1645)
    
    * fix(table): advance string upper bounds across surrogate gap
    
    * fix(table): trim carried string upper bound
---
 table/internal/utils.go      | 16 +++++++++++++---
 table/internal/utils_test.go | 32 +++++++++++++++++++++++++++++---
 2 files changed, 42 insertions(+), 6 deletions(-)

diff --git a/table/internal/utils.go b/table/internal/utils.go
index b52d91cd..d75637f2 100644
--- a/table/internal/utils.go
+++ b/table/internal/utils.go
@@ -538,17 +538,27 @@ func TruncateUpperBoundText(s string, trunc int) string {
 
 	result := []rune(s)[:trunc]
 	for i := len(result) - 1; i >= 0; i-- {
-		next := result[i] + 1
-		if utf8.ValidRune(next) {
+		if next, ok := nextValidRune(result[i]); ok {
 			result[i] = next
 
-			return string(result)
+			return string(result[:i+1])
 		}
 	}
 
 	return ""
 }
 
+func nextValidRune(r rune) (rune, bool) {
+	switch {
+	case r == '\uD7FF':
+		return '\uE000', true
+	case r >= utf8.MaxRune:
+		return 0, false
+	default:
+		return r + 1, true
+	}
+}
+
 func TruncateUpperBoundBinary(val []byte, trunc int) []byte {
 	if trunc >= len(val) {
 		return slices.Clone(val)
diff --git a/table/internal/utils_test.go b/table/internal/utils_test.go
index 603addff..0115cf1f 100644
--- a/table/internal/utils_test.go
+++ b/table/internal/utils_test.go
@@ -24,6 +24,7 @@ import (
 	"slices"
 	"testing"
 	"time"
+	"unicode/utf8"
 
 	"github.com/apache/arrow-go/v18/arrow/decimal128"
 	"github.com/apache/iceberg-go"
@@ -66,9 +67,34 @@ func TestMetricsModePairs(t *testing.T) {
 }
 
 func TestTruncateUpperBoundString(t *testing.T) {
-	assert.Equal(t, "ab", internal.TruncateUpperBoundText("aaaa", 2))
-	// \u10FFFF\u10FFFF\x00
-	assert.Equal(t, "", internal.TruncateUpperBoundText("\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf\x00", 2))
+	tests := []struct {
+		name     string
+		value    string
+		truncate int
+		expected string
+	}{
+		{name: "increment", value: "aaaa", truncate: 2, expected: "ab"},
+		{name: "surrogate gap", value: "\uD7FFx", truncate: 1, expected: "\uE000"},
+		{name: "preceding scalar", value: "\uD7FEx", truncate: 1, expected: "\uD7FF"},
+		{name: "carry past maximal suffix", value: string([]rune{'a', utf8.MaxRune, utf8.MaxRune, 'x'}), truncate: 3, expected: "b"},
+		{name: "carry to maximal scalar", value: string([]rune{utf8.MaxRune - 1, utf8.MaxRune, 'x'}), truncate: 2, expected: string([]rune{utf8.MaxRune})},
+		// U+10FFFF U+10FFFF NUL has no greater valid Unicode prefix.
+		{name: "all maximal", value: "\xf4\x8f\xbf\xbf\xf4\x8f\xbf\xbf\x00", truncate: 2, expected: ""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := internal.TruncateUpperBoundText(tt.value, tt.truncate)
+			assert.Equal(t, tt.expected, got)
+
+			if got != "" {
+				require.True(t, utf8.ValidString(got))
+				if tt.truncate < utf8.RuneCountInString(tt.value) {
+					require.Greater(t, bytes.Compare([]byte(got), []byte(tt.value)), 0)
+				}
+			}
+		})
+	}
 }
 
 func TestTruncateUpperBoundBinary(t *testing.T) {