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 a1637095 fix(arrow/csv): respect timestamp timezones (#1025)
a1637095 is described below

commit a163709505e7a430baac1ad5c87b513db65d97f7
Author: Minh Vu <[email protected]>
AuthorDate: Mon Jul 27 22:01:07 2026 +0200

    fix(arrow/csv): respect timestamp timezones (#1025)
    
    ### Rationale for this change
    
    Timestamp arrays can declare a timezone, but the CSV writer formats
    every value directly in UTC. This produces the wrong local timestamp and
    also lets invalid timezone declarations pass silently.
    
    ### What changes are included in this PR?
    
    * Use the timestamp type conversion function so values are formatted in
    the declared timezone.
    * Return invalid timezone errors from Writer.Write.
    * Propagate conversion errors from nested list values.
    
    ### Are these changes tested?
    
    Yes. The tests verify epoch formatting in America/New_York and rejection
    of an invalid timezone. All CSV tests and the assertion build pass with
    the required Parquet test data.
---
 arrow/csv/transformer.go | 19 ++++++++++++++-----
 arrow/csv/writer.go      |  5 ++++-
 arrow/csv/writer_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/arrow/csv/transformer.go b/arrow/csv/transformer.go
index 807050c8..5bcf26e0 100644
--- a/arrow/csv/transformer.go
+++ b/arrow/csv/transformer.go
@@ -27,11 +27,11 @@ import (
        "github.com/apache/arrow-go/v18/arrow/array"
 )
 
-func (w *Writer) transformColToStringArr(typ arrow.DataType, col arrow.Array, 
stringsReplacer func(string) string) []string {
+func (w *Writer) transformColToStringArr(typ arrow.DataType, col arrow.Array, 
stringsReplacer func(string) string) ([]string, error) {
        if w.customTypeConverter != nil {
                result, handled := w.customTypeConverter(typ, col)
                if handled {
-                       return result
+                       return result, nil
                }
        }
 
@@ -185,9 +185,13 @@ func (w *Writer) transformColToStringArr(typ 
arrow.DataType, col arrow.Array, st
        case *arrow.TimestampType:
                arr := col.(*array.Timestamp)
                t := typ.(*arrow.TimestampType)
+               toTime, err := t.GetToTimeFunc()
+               if err != nil {
+                       return nil, fmt.Errorf("arrow/csv: invalid timestamp 
timezone: %w", err)
+               }
                for i := 0; i < arr.Len(); i++ {
                        if arr.IsValid(i) {
-                               res[i] = 
arr.Value(i).ToTime(t.Unit).Format("2006-01-02 15:04:05.999999999")
+                               res[i] = 
toTime(arr.Value(i)).Format("2006-01-02 15:04:05.999999999")
                        } else {
                                res[i] = w.nullValue
                        }
@@ -227,7 +231,12 @@ func (w *Writer) transformColToStringArr(typ 
arrow.DataType, col arrow.Array, st
                        var b bytes.Buffer
                        b.Write([]byte{'{'})
                        writer := csv.NewWriter(&b)
-                       writer.Write(w.transformColToStringArr(list.DataType(), 
list, stringsReplacer))
+                       values, err := 
w.transformColToStringArr(list.DataType(), list, stringsReplacer)
+                       if err != nil {
+                               list.Release()
+                               return nil, err
+                       }
+                       writer.Write(values)
                        writer.Flush()
                        b.Truncate(b.Len() - 1)
                        b.Write([]byte{'}'})
@@ -277,5 +286,5 @@ func (w *Writer) transformColToStringArr(typ 
arrow.DataType, col arrow.Array, st
        default:
                panic(fmt.Errorf("arrow/csv: field has unsupported data type 
%s", typ.String()))
        }
-       return res
+       return res, nil
 }
diff --git a/arrow/csv/writer.go b/arrow/csv/writer.go
index f4891efd..5165d517 100644
--- a/arrow/csv/writer.go
+++ b/arrow/csv/writer.go
@@ -84,7 +84,10 @@ func (w *Writer) Write(record arrow.RecordBatch) error {
        }
 
        for j, col := range record.Columns() {
-               rows := w.transformColToStringArr(w.schema.Field(j).Type, col, 
w.stringReplacer)
+               rows, err := w.transformColToStringArr(w.schema.Field(j).Type, 
col, w.stringReplacer)
+               if err != nil {
+                       return err
+               }
                for i, row := range rows {
                        recs[i][j] = row
                }
diff --git a/arrow/csv/writer_test.go b/arrow/csv/writer_test.go
index 1701f999..66c664f8 100644
--- a/arrow/csv/writer_test.go
+++ b/arrow/csv/writer_test.go
@@ -664,3 +664,46 @@ func TestCustomTypeConversion(t *testing.T) {
        require.Equal(t, expected, buf.String())
 
 }
+
+func TestCSVWriterUsesTimestampTimezone(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       timestampType := &arrow.TimestampType{Unit: arrow.Second, TimeZone: 
"America/New_York"}
+       builder := array.NewTimestampBuilder(mem, timestampType)
+       builder.Append(0)
+       values := builder.NewTimestampArray()
+       builder.Release()
+       defer values.Release()
+
+       schema := arrow.NewSchema([]arrow.Field{{Name: "timestamp", Type: 
timestampType}}, nil)
+       record := array.NewRecordBatch(schema, []arrow.Array{values}, 1)
+       defer record.Release()
+
+       var output bytes.Buffer
+       writer := csv.NewWriter(&output, schema)
+       require.NoError(t, writer.Write(record))
+       require.NoError(t, writer.Flush())
+       assert.Equal(t, "1969-12-31 19:00:00\n", output.String())
+}
+
+func TestCSVWriterRejectsInvalidTimestampTimezone(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       timestampType := &arrow.TimestampType{Unit: arrow.Second, TimeZone: 
"invalid/timezone"}
+       builder := array.NewTimestampBuilder(mem, timestampType)
+       builder.Append(0)
+       values := builder.NewTimestampArray()
+       builder.Release()
+       defer values.Release()
+
+       schema := arrow.NewSchema([]arrow.Field{{Name: "timestamp", Type: 
timestampType}}, nil)
+       record := array.NewRecordBatch(schema, []arrow.Array{values}, 1)
+       defer record.Release()
+
+       writer := csv.NewWriter(io.Discard, schema)
+       err := writer.Write(record)
+       require.Error(t, err)
+       assert.ErrorContains(t, err, "invalid timestamp timezone")
+}

Reply via email to