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) {