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")
+}