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/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c72476d fix(parquet/pqarrow): preserve INT96 timestamps across units 
(#1006)
8c72476d is described below

commit 8c72476df63df6d3d29b938ca528e1526985370b
Author: Minh Vu <[email protected]>
AuthorDate: Mon Jul 27 20:57:18 2026 +0200

    fix(parquet/pqarrow): preserve INT96 timestamps across units (#1006)
    
    ### Rationale for this change
    
    INT96 timestamp conversion currently routes values through
    `time.Duration` and applies a nanoseconds-per-day modulus directly to
    the source value. This produces incorrect results for second,
    millisecond, and microsecond timestamps, overflows for timestamps
    outside the duration range, and leaves negative timestamps with a
    negative time-of-day remainder.
    
    ### What changes are included in this PR?
    
    - Convert each Arrow timestamp unit using integer units-per-day and
    nanoseconds-per-unit constants.
    - Normalize negative remainders onto the preceding Julian day.
    - Add focused coverage for every timestamp unit on both sides of the
    Unix epoch.
    
    ### Are these changes tested?
    
    Yes. The focused pqarrow tests pass.
---
 parquet/pqarrow/encode_arrow.go            | 24 ++++++++------
 parquet/pqarrow/encode_arrow_int96_test.go | 51 ++++++++++++++++++++++++++++++
 2 files changed, 66 insertions(+), 9 deletions(-)

diff --git a/parquet/pqarrow/encode_arrow.go b/parquet/pqarrow/encode_arrow.go
index 9ec11ff8..c7f089c2 100644
--- a/parquet/pqarrow/encode_arrow.go
+++ b/parquet/pqarrow/encode_arrow.go
@@ -24,7 +24,6 @@ import (
        "errors"
        "fmt"
        "math"
-       "time"
        "unsafe"
 
        "github.com/apache/arrow-go/v18/arrow"
@@ -712,20 +711,27 @@ const (
 )
 
 func arrowTimestampToImpalaTimestamp(unit arrow.TimeUnit, t int64, out 
*parquet.Int96) {
-       var d time.Duration
+       var unitsPerDay, nanosPerUnit int64
        switch unit {
        case arrow.Second:
-               d = time.Duration(t) * time.Second
-       case arrow.Microsecond:
-               d = time.Duration(t) * time.Microsecond
+               unitsPerDay, nanosPerUnit = 24*60*60, 1000*1000*1000
        case arrow.Millisecond:
-               d = time.Duration(t) * time.Millisecond
+               unitsPerDay, nanosPerUnit = 24*60*60*1000, 1000*1000
+       case arrow.Microsecond:
+               unitsPerDay, nanosPerUnit = 24*60*60*1000*1000, 1000
        case arrow.Nanosecond:
-               d = time.Duration(t) * time.Nanosecond
+               unitsPerDay, nanosPerUnit = nanoSecondsPerDay, 1
+       }
+
+       days := t / unitsPerDay
+       remainder := t % unitsPerDay
+       if remainder < 0 {
+               days--
+               remainder += unitsPerDay
        }
 
-       julianDays := (int64(d.Hours()) / 24) + julianEpochOffsetDays
-       lastDayNanos := t % (nanoSecondsPerDay)
+       julianDays := days + julianEpochOffsetDays
+       lastDayNanos := remainder * nanosPerUnit
        binary.LittleEndian.PutUint64((*out)[:8], uint64(lastDayNanos))
        binary.LittleEndian.PutUint32((*out)[8:], uint32(julianDays))
 }
diff --git a/parquet/pqarrow/encode_arrow_int96_test.go 
b/parquet/pqarrow/encode_arrow_int96_test.go
new file mode 100644
index 00000000..4e5cbf2d
--- /dev/null
+++ b/parquet/pqarrow/encode_arrow_int96_test.go
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pqarrow
+
+import (
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestArrowTimestampToImpalaTimestamp(t *testing.T) {
+       tests := []struct {
+               name  string
+               unit  arrow.TimeUnit
+               value arrow.Timestamp
+       }{
+               {"seconds after epoch", arrow.Second, 946_684_801},
+               {"seconds before epoch", arrow.Second, -1},
+               {"milliseconds after epoch", arrow.Millisecond, 
946_684_800_001},
+               {"milliseconds before epoch", arrow.Millisecond, -1},
+               {"microseconds after epoch", arrow.Microsecond, 
946_684_800_000_001},
+               {"microseconds before epoch", arrow.Microsecond, -1},
+               {"nanoseconds after epoch", arrow.Nanosecond, 
946_684_800_000_000_001},
+               {"nanoseconds before epoch", arrow.Nanosecond, -1},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       var got parquet.Int96
+                       arrowTimestampToImpalaTimestamp(tt.unit, 
int64(tt.value), &got)
+
+                       assert.Equal(t, tt.value.ToTime(tt.unit), got.ToTime())
+               })
+       }
+}

Reply via email to