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

laskoviymishka 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 0b0828bff perf(rest): parse numeric scan partition values directly 
(#2115)
0b0828bff is described below

commit 0b0828bffd8ba7f50e18cbafa59d6a4b8e5df2f6
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 6 10:28:44 2026 +0200

    perf(rest): parse numeric scan partition values directly (#2115)
    
    * perf(rest): parse numeric scan partition values directly
    
    * fix(rest): preserve quoted numeric partition values
---
 catalog/rest/scan_task_decoder.go            | 35 +++++++----
 catalog/rest/scan_task_decoder_bench_test.go | 71 ++++++++++++++++++++++-
 catalog/rest/scan_task_decoder_test.go       | 87 ++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 13 deletions(-)

diff --git a/catalog/rest/scan_task_decoder.go 
b/catalog/rest/scan_task_decoder.go
index ba7f9aa7e..82e833647 100644
--- a/catalog/rest/scan_task_decoder.go
+++ b/catalog/rest/scan_task_decoder.go
@@ -26,6 +26,7 @@ import (
        "math"
        "reflect"
        "slices"
+       "strconv"
 
        "github.com/apache/iceberg-go"
        "github.com/apache/iceberg-go/table"
@@ -763,13 +764,18 @@ func decodePartitionLiteral(raw json.RawMessage, typ 
iceberg.Type) (iceberg.Lite
 }
 
 func decodeJSONInteger(raw json.RawMessage, bitSize int) (int64, error) {
-       dec := json.NewDecoder(bytes.NewReader(raw))
-       dec.UseNumber()
-       var value json.Number
-       if err := dec.Decode(&value); err != nil {
-               return 0, fmt.Errorf("invalid integer value: %w", err)
+       value := bytes.Trim(raw, " \t\r\n")
+       if !json.Valid(value) {
+               return 0, fmt.Errorf("invalid integer value: invalid JSON 
number %q", raw)
        }
-       parsed, err := value.Int64()
+       if len(value) > 0 && value[0] == '"' {
+               var number json.Number
+               if err := json.Unmarshal(value, &number); err != nil {
+                       return 0, fmt.Errorf("invalid integer value: %w", err)
+               }
+               value = []byte(number)
+       }
+       parsed, err := strconv.ParseInt(string(value), 10, 64)
        if err != nil {
                return 0, fmt.Errorf("invalid integer value %q: %w", value, err)
        }
@@ -781,13 +787,18 @@ func decodeJSONInteger(raw json.RawMessage, bitSize int) 
(int64, error) {
 }
 
 func decodeJSONFloat(raw json.RawMessage, bitSize int) (float64, error) {
-       dec := json.NewDecoder(bytes.NewReader(raw))
-       dec.UseNumber()
-       var value json.Number
-       if err := dec.Decode(&value); err != nil {
-               return 0, fmt.Errorf("invalid floating-point value: %w", err)
+       value := bytes.Trim(raw, " \t\r\n")
+       if !json.Valid(value) {
+               return 0, fmt.Errorf("invalid floating-point value: invalid 
JSON number %q", raw)
+       }
+       if len(value) > 0 && value[0] == '"' {
+               var number json.Number
+               if err := json.Unmarshal(value, &number); err != nil {
+                       return 0, fmt.Errorf("invalid floating-point value: 
%w", err)
+               }
+               value = []byte(number)
        }
-       parsed, err := value.Float64()
+       parsed, err := strconv.ParseFloat(string(value), 64)
        if err != nil {
                return 0, fmt.Errorf("invalid floating-point value %q: %w", 
value, err)
        }
diff --git a/catalog/rest/scan_task_decoder_bench_test.go 
b/catalog/rest/scan_task_decoder_bench_test.go
index c7130484d..6ac8466db 100644
--- a/catalog/rest/scan_task_decoder_bench_test.go
+++ b/catalog/rest/scan_task_decoder_bench_test.go
@@ -18,11 +18,16 @@
 package rest
 
 import (
+       "encoding/json"
        "fmt"
        "testing"
 )
 
-var decodeScanTasksBenchmarkSink int
+var (
+       decodeScanTasksBenchmarkSink   int
+       decodeJSONIntegerBenchmarkSink int64
+       decodeJSONFloatBenchmarkSink   float64
+)
 
 func BenchmarkDecodeScanTasksDeletionVectors(b *testing.B) {
        metadata := newScanTaskDecoderMetadata()
@@ -109,3 +114,67 @@ func BenchmarkDecodeScanTasks(b *testing.B) {
                })
        }
 }
+
+func BenchmarkDecodeJSONInteger(b *testing.B) {
+       raw := json.RawMessage("12345")
+       for _, bitSize := range []int{32, 64} {
+               b.Run(fmt.Sprintf("int%d", bitSize), func(b *testing.B) {
+                       b.ReportAllocs()
+                       for b.Loop() {
+                               value, err := decodeJSONInteger(raw, bitSize)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               decodeJSONIntegerBenchmarkSink = value
+                       }
+               })
+       }
+}
+
+func BenchmarkDecodeJSONFloat(b *testing.B) {
+       raw := json.RawMessage("12345.25")
+       for _, bitSize := range []int{32, 64} {
+               b.Run(fmt.Sprintf("float%d", bitSize), func(b *testing.B) {
+                       b.ReportAllocs()
+                       for b.Loop() {
+                               value, err := decodeJSONFloat(raw, bitSize)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               decodeJSONFloatBenchmarkSink = value
+                       }
+               })
+       }
+}
+
+func BenchmarkDecodeJSONNumberForms(b *testing.B) {
+       for _, tc := range []struct {
+               name, raw string
+               floating  bool
+       }{
+               {name: "int64_exact", raw: "9007199254740993"},
+               {name: "quoted_int64", raw: `"9007199254740993"`},
+               {name: "exponent", raw: "-1.25e2", floating: true},
+               {name: "quoted_exponent", raw: `"-1.25e2"`, floating: true},
+       } {
+               b.Run(tc.name, func(b *testing.B) {
+                       raw := json.RawMessage(tc.raw)
+                       b.ReportAllocs()
+                       for b.Loop() {
+                               if tc.floating {
+                                       value, err := decodeJSONFloat(raw, 64)
+                                       if err != nil {
+                                               b.Fatal(err)
+                                       }
+                                       decodeJSONFloatBenchmarkSink = value
+                               } else {
+                                       value, err := decodeJSONInteger(raw, 64)
+                                       if err != nil {
+                                               b.Fatal(err)
+                                       }
+                                       decodeJSONIntegerBenchmarkSink = value
+                               }
+                       }
+               })
+       }
+}
diff --git a/catalog/rest/scan_task_decoder_test.go 
b/catalog/rest/scan_task_decoder_test.go
index b796c4a17..e4d624d7a 100644
--- a/catalog/rest/scan_task_decoder_test.go
+++ b/catalog/rest/scan_task_decoder_test.go
@@ -630,6 +630,93 @@ func TestDecodePartitionLiteralRejectsInvalidValues(t 
*testing.T) {
        }
 }
 
+func TestDecodeJSONInteger(t *testing.T) {
+       t.Parallel()
+
+       for _, tt := range []struct {
+               name    string
+               raw     string
+               bitSize int
+               want    int64
+               wantErr string
+       }{
+               {name: "int64 max", raw: `9223372036854775807`, bitSize: 64, 
want: math.MaxInt64},
+               {name: "int64 min with whitespace", raw: ` -9223372036854775808 
`, bitSize: 64, want: math.MinInt64},
+               {name: "int32 max", raw: `2147483647`, bitSize: 32, want: 
math.MaxInt32},
+               {name: "int32 min", raw: `-2147483648`, bitSize: 32, want: 
math.MinInt32},
+               {name: "above float precision", raw: `9007199254740993`, 
bitSize: 64, want: 9007199254740993},
+               {name: "quoted integer", raw: `"34"`, bitSize: 32, want: 34},
+               {name: "escaped integer", raw: `"\u0033\u0034"`, bitSize: 64, 
want: 34},
+               {name: "quoted overflow", raw: `"9223372036854775808"`, 
bitSize: 64, wantErr: "invalid integer value"},
+               {name: "invalid quoted number", raw: `"not-a-number"`, bitSize: 
64, wantErr: "invalid integer value"},
+               {name: "non JSON whitespace", raw: "\u00a01\u00a0", bitSize: 
64, wantErr: "invalid JSON number"},
+               {name: "fraction", raw: `1.5`, bitSize: 64, wantErr: "invalid 
integer value"},
+               {name: "exponent", raw: `1e2`, bitSize: 64, wantErr: "invalid 
integer value"},
+               {name: "int64 overflow", raw: `9223372036854775808`, bitSize: 
64, wantErr: "invalid integer value"},
+               {name: "int32 overflow", raw: `2147483648`, bitSize: 32, 
wantErr: "outside int32 range"},
+               {name: "leading plus", raw: `+1`, bitSize: 64, wantErr: 
"invalid JSON number"},
+               {name: "leading zero", raw: `01`, bitSize: 64, wantErr: 
"invalid JSON number"},
+               {name: "trailing value", raw: `1 2`, bitSize: 64, wantErr: 
"invalid JSON number"},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       got, err := decodeJSONInteger(json.RawMessage(tt.raw), 
tt.bitSize)
+                       if tt.wantErr != "" {
+                               require.ErrorContains(t, err, tt.wantErr)
+
+                               return
+                       }
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.want, got)
+               })
+       }
+}
+
+func TestDecodeJSONFloat(t *testing.T) {
+       t.Parallel()
+
+       for _, tt := range []struct {
+               name        string
+               raw         string
+               bitSize     int
+               want        float64
+               wantSignbit bool
+               wantErr     string
+       }{
+               {name: "exponent with whitespace", raw: ` -1.25e2 `, bitSize: 
64, want: -125},
+               {name: "negative zero", raw: `-0`, bitSize: 64, wantSignbit: 
true},
+               {name: "float32 range", raw: `3.4028234e38`, bitSize: 32, want: 
3.4028234e38},
+               {name: "float32 overflow", raw: `3.5e38`, bitSize: 32, wantErr: 
"outside float32 range"},
+               {name: "quoted float", raw: `"1.25"`, bitSize: 64, want: 1.25},
+               {name: "escaped float", raw: `"\u0031.25"`, bitSize: 32, want: 
1.25},
+               {name: "quoted exponent", raw: `"-1.25e2"`, bitSize: 64, want: 
-125},
+               {name: "quoted negative zero", raw: `"-0"`, bitSize: 64, 
wantSignbit: true},
+               {name: "quoted overflow", raw: `"1e999"`, bitSize: 64, wantErr: 
"invalid floating-point value"},
+               {name: "invalid quoted number", raw: `"NaN"`, bitSize: 64, 
wantErr: "invalid floating-point value"},
+               {name: "non JSON whitespace", raw: "\u00a01.25\u00a0", bitSize: 
64, wantErr: "invalid JSON number"},
+               {name: "overflow", raw: `1e999`, bitSize: 64, wantErr: "invalid 
floating-point value"},
+               {name: "leading plus", raw: `+1.5`, bitSize: 64, wantErr: 
"invalid JSON number"},
+               {name: "trailing value", raw: `1.5 2`, bitSize: 64, wantErr: 
"invalid JSON number"},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       got, err := decodeJSONFloat(json.RawMessage(tt.raw), 
tt.bitSize)
+                       if tt.wantErr != "" {
+                               require.ErrorContains(t, err, tt.wantErr)
+
+                               return
+                       }
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.want, got)
+                       if tt.wantSignbit {
+                               assert.True(t, math.Signbit(got))
+                       }
+               })
+       }
+}
+
 func validScanTasksWire() ScanTasks {
        data := RESTContentFile{
                SpecID:          7,

Reply via email to