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 1e0b757e fix(arrow/csv): append null for later parse failures (#1149)
1e0b757e is described below
commit 1e0b757e146da4fb08c29fb6f62c0104d38af4fa
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 14 18:10:47 2026 +0200
fix(arrow/csv): append null for later parse failures (#1149)
## What
CSV scalar parsers already append a null when their own parse fails, but
several only did so while the reader had no earlier error. A malformed
value in a later column could therefore append the parser zero value
after another column failed. This keeps the first error and appends null
for every failed scalar conversion.
## Test
- go test ./arrow/csv -run
TestCSVReaderAppendsNullAfterPreviousParseError -count=1
---
arrow/csv/example_csv_test.go | 17 +++++-
arrow/csv/reader.go | 127 ++++++++++++++++++++++++------------------
arrow/csv/reader_test.go | 64 +++++++++++++++++++++
3 files changed, 152 insertions(+), 56 deletions(-)
diff --git a/arrow/csv/example_csv_test.go b/arrow/csv/example_csv_test.go
index 268e6325..6e642892 100644
--- a/arrow/csv/example_csv_test.go
+++ b/arrow/csv/example_csv_test.go
@@ -46,7 +46,7 @@ func Example_reader() {
{Name: "c7", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
{Name: "c8", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
{Name: "c9", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
- {Name: "c10", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
+ {Name: "c10", Type: arrow.PrimitiveTypes.Uint64, Nullable:
true},
{Name: "c11", Type: arrow.PrimitiveTypes.Float64, Nullable:
true},
{Name: "c12", Type: arrow.PrimitiveTypes.Float64, Nullable:
true},
{Name: "c13", Type: arrow.BinaryTypes.String, Nullable: true},
@@ -64,6 +64,10 @@ func Example_reader() {
fmt.Println("No records found")
return
}
+ if err := reader.Err(); err != nil {
+ fmt.Printf("Error reading CSV: %v\n", err)
+ return
+ }
// Get the record but don't release it - the reader will handle that
record := reader.RecordBatch()
@@ -73,7 +77,7 @@ func Example_reader() {
fmt.Println()
fmt.Println("Basic statistics for numeric columns:")
- for i := 1; i < 10; i++ { // cols c2 through c10 are Int64
+ for i := 1; i < 9; i++ { // cols c2 through c9 are Int64
col := record.Column(i).(*array.Int64)
var sum int64
for j := 0; j < col.Len(); j++ {
@@ -83,6 +87,13 @@ func Example_reader() {
fmt.Printf("Column c%d: Average = %.2f\n", i+1, avg)
}
+ col := record.Column(9).(*array.Uint64)
+ var sum float64
+ for j := 0; j < col.Len(); j++ {
+ sum += float64(col.Value(j))
+ }
+ fmt.Printf("Column c10: Average = %.2f\n", sum/float64(col.Len()))
+
for i := 10; i < 12; i++ { // cols c11 and c12 are Float64
col := record.Column(i).(*array.Float64)
var sum float64
@@ -106,7 +117,7 @@ func Example_reader() {
// Column c7: Average = 130.60
// Column c8: Average = 30176.41
// Column c9: Average = 2220897700.60
- // Column c10: Average = -86834033398685392.00
+ // Column c10: Average = 8652627809237404672.00
// Column c11: Average = 0.4793
// Column c12: Average = 0.5090
}
diff --git a/arrow/csv/reader.go b/arrow/csv/reader.go
index 9387f23d..f2a1f0b2 100644
--- a/arrow/csv/reader.go
+++ b/arrow/csv/reader.go
@@ -404,6 +404,12 @@ func (r *Reader) isNull(val string) bool {
return false
}
+func (r *Reader) setParseError(err error) {
+ if r.err == nil {
+ r.err = err
+ }
+}
+
func (r *Reader) read(recs []string) {
for i, str := range recs {
r.fieldConverter[i](str)
@@ -551,7 +557,7 @@ func (r *Reader) parseBool(field array.Builder, str string)
{
v, err := strconv.ParseBool(str)
if err != nil {
- r.err = fmt.Errorf("%w: unrecognized boolean: %s", err, str)
+ r.setParseError(fmt.Errorf("%w: unrecognized boolean: %s", err,
str))
field.AppendNull()
return
}
@@ -566,8 +572,8 @@ func (r *Reader) parseInt8(field array.Builder, str string)
{
}
v, err := strconv.ParseInt(str, 10, 8)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -582,8 +588,8 @@ func (r *Reader) parseInt16(field array.Builder, str
string) {
}
v, err := strconv.ParseInt(str, 10, 16)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -598,8 +604,8 @@ func (r *Reader) parseInt32(field array.Builder, str
string) {
}
v, err := strconv.ParseInt(str, 10, 32)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -614,8 +620,8 @@ func (r *Reader) parseInt64(field array.Builder, str
string) {
}
v, err := strconv.ParseInt(str, 10, 64)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -630,8 +636,8 @@ func (r *Reader) parseUint8(field array.Builder, str
string) {
}
v, err := strconv.ParseUint(str, 10, 8)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -646,8 +652,8 @@ func (r *Reader) parseUint16(field array.Builder, str
string) {
}
v, err := strconv.ParseUint(str, 10, 16)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -662,8 +668,8 @@ func (r *Reader) parseUint32(field array.Builder, str
string) {
}
v, err := strconv.ParseUint(str, 10, 32)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -678,8 +684,8 @@ func (r *Reader) parseUint64(field array.Builder, str
string) {
}
v, err := strconv.ParseUint(str, 10, 64)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -694,8 +700,8 @@ func (r *Reader) parseFloat16(field array.Builder, str
string) {
}
v, err := strconv.ParseFloat(str, 32)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -709,8 +715,8 @@ func (r *Reader) parseFloat32(field array.Builder, str
string) {
}
v, err := strconv.ParseFloat(str, 32)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -724,8 +730,8 @@ func (r *Reader) parseFloat64(field array.Builder, str
string) {
}
v, err := strconv.ParseFloat(str, 64)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -740,8 +746,8 @@ func (r *Reader) parseTimestamp(field array.Builder, str
string, unit arrow.Time
}
v, err := arrow.TimestampFromString(str, unit)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -756,8 +762,8 @@ func (r *Reader) parseDate32(field array.Builder, str
string) {
}
tm, err := time.Parse("2006-01-02", str)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -771,8 +777,8 @@ func (r *Reader) parseDate64(field array.Builder, str
string) {
}
tm, err := time.Parse("2006-01-02", str)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -786,8 +792,8 @@ func (r *Reader) parseTime32(field array.Builder, str
string, unit arrow.TimeUni
}
val, err := arrow.Time32FromString(str, unit)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -801,8 +807,8 @@ func (r *Reader) parseDecimal128(field array.Builder, str
string, prec, scale in
}
val, err := decimal128.FromString(str, prec, scale)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -816,8 +822,8 @@ func (r *Reader) parseDecimal256(field array.Builder, str
string, prec, scale in
}
val, err := decimal256.FromString(str, prec, scale)
- if err != nil && r.err == nil {
- r.err = err
+ if err != nil {
+ r.setParseError(err)
field.AppendNull()
return
}
@@ -830,23 +836,26 @@ func (r *Reader) parseListLike(field
array.ListLikeBuilder, str string) {
return
}
if !strings.HasPrefix(str, "{") || !strings.HasSuffix(str, "}") {
- r.err = errors.New("invalid list format. should start with '{'
and end with '}'")
+ r.setParseError(errors.New("invalid list format. should start
with '{' and end with '}'"))
+ field.AppendNull()
return
}
str = strings.Trim(str, "{}")
- field.Append(true)
if len(str) == 0 {
// we don't want to create the csv reader if we already know the
// string is empty
+ field.Append(true)
return
}
- valueBldr := field.ValueBuilder()
reader := csv.NewReader(strings.NewReader(str))
items, err := reader.Read()
if err != nil {
- r.err = err
+ r.setParseError(err)
+ field.AppendNull()
return
}
+ field.Append(true)
+ valueBldr := field.ValueBuilder()
for _, str := range items {
r.initFieldConverter(valueBldr)(str)
}
@@ -858,29 +867,39 @@ func (r *Reader) parseFixedSizeList(field
*array.FixedSizeListBuilder, str strin
return
}
if !strings.HasPrefix(str, "{") || !strings.HasSuffix(str, "}") {
- r.err = errors.New("invalid list format. should start with '{'
and end with '}'")
+ r.setParseError(errors.New("invalid list format. should start
with '{' and end with '}'"))
+ field.AppendNull()
return
}
str = strings.Trim(str, "{}")
- field.Append(true)
if len(str) == 0 {
// we don't want to create the csv reader if we already know the
// string is empty
+ if n != 0 {
+ r.setParseError(fmt.Errorf("%w: fixed size list items
should match the fixed size list length, expected %d, got 0", arrow.ErrInvalid,
n))
+ field.AppendNull()
+ return
+ }
+ field.Append(true)
return
}
valueBldr := field.ValueBuilder()
reader := csv.NewReader(strings.NewReader(str))
items, err := reader.Read()
if err != nil {
- r.err = err
+ r.setParseError(err)
+ field.AppendNull()
return
}
- if len(items) == n {
- for _, str := range items {
- r.initFieldConverter(valueBldr)(str)
- }
- } else {
- r.err = fmt.Errorf("%w: fixed size list items should match the
fixed size list length, expected %d, got %d", arrow.ErrInvalid, n, len(items))
+ if len(items) != n {
+ r.setParseError(fmt.Errorf("%w: fixed size list items should
match the fixed size list length, expected %d, got %d", arrow.ErrInvalid, n,
len(items)))
+ field.AppendNull()
+ return
+ }
+
+ field.Append(true)
+ for _, str := range items {
+ r.initFieldConverter(valueBldr)(str)
}
}
@@ -892,7 +911,7 @@ func (r *Reader) parseBinaryType(field array.Builder, str
string) {
}
decodedVal, err := base64.StdEncoding.DecodeString(str)
if err != nil {
- r.err = fmt.Errorf("cannot decode base64 string %s", str)
+ r.setParseError(fmt.Errorf("cannot decode base64 string %s",
str))
field.AppendNull()
return
}
@@ -908,7 +927,7 @@ func (r *Reader) parseLargeBinaryType(field array.Builder,
str string) {
}
decodedVal, err := base64.StdEncoding.DecodeString(str)
if err != nil {
- r.err = fmt.Errorf("cannot decode base64 string %s", str)
+ r.setParseError(fmt.Errorf("cannot decode base64 string %s",
str))
field.AppendNull()
return
}
@@ -924,7 +943,7 @@ func (r *Reader) parseFixedSizeBinaryType(field
array.Builder, str string, byteW
}
decodedVal, err := base64.StdEncoding.DecodeString(str)
if err != nil {
- r.err = fmt.Errorf("cannot decode base64 string %s", str)
+ r.setParseError(fmt.Errorf("cannot decode base64 string %s",
str))
field.AppendNull()
return
}
@@ -932,7 +951,8 @@ func (r *Reader) parseFixedSizeBinaryType(field
array.Builder, str string, byteW
if len(decodedVal) == byteWidth {
field.(*array.FixedSizeBinaryBuilder).Append(decodedVal)
} else {
- r.err = fmt.Errorf("%w: the length of fixed size binary value
should match the fixed size binary byte width, expected %d, got %d",
arrow.ErrInvalid, byteWidth, len(decodedVal))
+ r.setParseError(fmt.Errorf("%w: the length of fixed size binary
value should match the fixed size binary byte width, expected %d, got %d",
arrow.ErrInvalid, byteWidth, len(decodedVal)))
+ field.AppendNull()
}
}
@@ -942,7 +962,8 @@ func (r *Reader) parseExtension(field array.Builder, str
string) {
return
}
if err := field.AppendValueFromString(str); err != nil {
- r.err = err
+ r.setParseError(err)
+ field.AppendNull()
return
}
}
diff --git a/arrow/csv/reader_test.go b/arrow/csv/reader_test.go
index c7cd61f4..8623f70b 100644
--- a/arrow/csv/reader_test.go
+++ b/arrow/csv/reader_test.go
@@ -901,6 +901,70 @@ func benchRead(b *testing.B, raw []byte, rows, cols,
chunks int) {
}
}
+func TestCSVReaderAppendsNullAfterPreviousParseError(t *testing.T) {
+ schema := arrow.NewSchema([]arrow.Field{
+ {Name: "int8", Type: arrow.PrimitiveTypes.Int8},
+ {Name: "bool", Type: arrow.FixedWidthTypes.Boolean},
+ {Name: "int16", Type: arrow.PrimitiveTypes.Int16},
+ {Name: "int32", Type: arrow.PrimitiveTypes.Int32},
+ {Name: "int64", Type: arrow.PrimitiveTypes.Int64},
+ {Name: "uint8", Type: arrow.PrimitiveTypes.Uint8},
+ {Name: "uint16", Type: arrow.PrimitiveTypes.Uint16},
+ {Name: "uint32", Type: arrow.PrimitiveTypes.Uint32},
+ {Name: "uint64", Type: arrow.PrimitiveTypes.Uint64},
+ {Name: "float16", Type: arrow.FixedWidthTypes.Float16},
+ {Name: "float32", Type: arrow.PrimitiveTypes.Float32},
+ {Name: "float64", Type: arrow.PrimitiveTypes.Float64},
+ {Name: "timestamp", Type: arrow.FixedWidthTypes.Timestamp_ms},
+ {Name: "date32", Type: arrow.PrimitiveTypes.Date32},
+ {Name: "date64", Type: arrow.PrimitiveTypes.Date64},
+ {Name: "decimal128", Type: &arrow.Decimal128Type{Precision: 10,
Scale: 2}},
+ {Name: "decimal256", Type: &arrow.Decimal256Type{Precision: 10,
Scale: 2}},
+ }, nil)
+
+ r := csv.NewReader(strings.NewReader(strings.Repeat("bad,",
schema.NumFields()-1)+"bad\n"), schema)
+ defer r.Release()
+
+ require.True(t, r.Next())
+ require.Error(t, r.Err())
+ for i, col := range r.RecordBatch().Columns() {
+ require.Truef(t, col.IsNull(0), "column %d (%s) should be
null", i, col.DataType())
+ }
+}
+
+func TestCSVReaderAppendsNullAfterCompositeParseError(t *testing.T) {
+ tests := []struct {
+ name string
+ typ arrow.DataType
+ value string
+ }{
+ {name: "list format", typ:
arrow.ListOf(arrow.PrimitiveTypes.Int8), value: "bad"},
+ {name: "fixed size list format", typ: arrow.FixedSizeListOf(2,
arrow.PrimitiveTypes.Int8), value: "bad"},
+ {name: "fixed size list length", typ: arrow.FixedSizeListOf(2,
arrow.PrimitiveTypes.Int8), value: "{1}"},
+ {name: "binary", typ: arrow.BinaryTypes.Binary, value: "%%%"},
+ {name: "large binary", typ: arrow.BinaryTypes.LargeBinary,
value: "%%%"},
+ {name: "fixed size binary format", typ:
&arrow.FixedSizeBinaryType{ByteWidth: 3}, value: "%%%"},
+ {name: "fixed size binary", typ:
&arrow.FixedSizeBinaryType{ByteWidth: 3}, value: "AQ=="},
+ {name: "extension", typ: extensions.NewUUIDType(), value:
"bad"},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ schema := arrow.NewSchema([]arrow.Field{
+ {Name: "int8", Type: arrow.PrimitiveTypes.Int8},
+ {Name: "value", Type: tc.typ},
+ }, nil)
+ r :=
csv.NewReader(strings.NewReader("bad;"+tc.value+"\n"), schema,
csv.WithComma(';'))
+ defer r.Release()
+
+ require.True(t, r.Next())
+ require.ErrorContains(t, r.Err(), "strconv.ParseInt")
+ require.True(t, r.RecordBatch().Column(0).IsNull(0))
+ require.True(t, r.RecordBatch().Column(1).IsNull(0))
+ })
+ }
+}
+
func TestInferringSchema(t *testing.T) {
var b bytes.Buffer
wr := stdcsv.NewWriter(&b)