Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
36599a8
Separate public API from impl of comparison functions
serramatutu Apr 30, 2026
29b75f0
Add `nullable` to `equalOption`
serramatutu Apr 30, 2026
fbfe0ca
Check for nullable opt in most array compare implementations
serramatutu Apr 30, 2026
1da53ae
Add stricter tests to null JSON in record and struct
serramatutu Apr 28, 2026
7dc69ee
`AppendEmptyValue()` if field is nullable
serramatutu Apr 28, 2026
554eddc
Add `nullable` argument to `GetOneForMarshal()`
serramatutu Apr 29, 2026
6fb4438
Make struct and record use `field.Nullable` when serializing
serramatutu Apr 29, 2026
ad78a63
Fix tests that were implicitly depending on wrong nullable semantics
serramatutu Apr 29, 2026
0cbfe2a
Post-rebase fixes for CI: JSON int precision, GetOneForMarshal, TinyGo
zeroshade Jul 2, 2026
719f973
Make nested nullability field-local in comparison and union marshal
zeroshade Jul 2, 2026
98c1bbe
Make exact Equal and chunked comparison field-local too
zeroshade Jul 2, 2026
c084d40
Preserve nullability option in exact chunked comparison
zeroshade Jul 2, 2026
39d9f56
Address review feedback on nullability-aware comparison
zeroshade Jul 2, 2026
13e84dd
Compare record/table fields ignoring metadata
zeroshade Jul 3, 2026
4590470
Address roborev findings on record/table field comparison
zeroshade Jul 3, 2026
94644b3
Avoid breaking arrow.Array with optional NullableMarshaler
zeroshade Jul 4, 2026
a474f1f
Thread field-local nullability through list, union, and REE
zeroshade Jul 9, 2026
45fef5e
Make run-end-encoded value comparison symmetric in ValueNullable
zeroshade Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions arrow/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ type Array interface {
Release()
}

// NullableMarshaler is an optional interface an Array may implement to control
// whether a value is rendered as JSON null based on the field-local nullability
// from the schema, rather than solely on the array's own validity bitmap.
//
// It exists so containers (struct/union/dictionary/run-end-encoded/extension)
// and RecordToJSON can propagate each field's Nullable flag down to its values
// without a breaking change to the Array interface. Arrays that do not implement
// it fall back to GetOneForMarshal, preserving the previous validity-based behavior.
type NullableMarshaler interface {
// GetOneForMarshalNullable returns the value at i for json.Marshal. When
// nullable is false, a value is returned even if the array's validity bitmap
// marks it null (a non-nullable field must not serialize as null).
GetOneForMarshalNullable(i int, nullable bool) interface{}
}

// ValueType is a generic constraint for valid Arrow primitive types
type ValueType interface {
bool | FixedWidthType | string | []byte
Expand Down
36 changes: 24 additions & 12 deletions arrow/array/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,17 @@ func (a *Binary) setData(data *Data) {
}
}

func (a *Binary) GetOneForMarshal(i int) interface{} {
if a.IsNull(i) {
func (a *Binary) GetOneForMarshalNullable(i int, nullable bool) interface{} {
if nullable && a.IsNull(i) {
return nil
}
return a.Value(i)
}

func (a *Binary) GetOneForMarshal(i int) interface{} {
return a.GetOneForMarshalNullable(i, true)
}

func (a *Binary) MarshalJSON() ([]byte, error) {
vals := make([]interface{}, a.Len())
for i := 0; i < a.Len(); i++ {
Expand Down Expand Up @@ -223,9 +227,9 @@ func (a *Binary) ValidateFull() error {
return nil
}

func arrayEqualBinary(left, right *Binary) bool {
func arrayEqualBinary(left, right *Binary, opt equalOption) bool {
for i := 0; i < left.Len(); i++ {
if left.IsNull(i) {
if opt.nullable && left.IsNull(i) {
continue
}
if !bytes.Equal(left.Value(i), right.Value(i)) {
Expand Down Expand Up @@ -346,13 +350,17 @@ func (a *LargeBinary) setData(data *Data) {
}
}

func (a *LargeBinary) GetOneForMarshal(i int) interface{} {
if a.IsNull(i) {
func (a *LargeBinary) GetOneForMarshalNullable(i int, nullable bool) interface{} {
if nullable && a.IsNull(i) {
return nil
}
return a.Value(i)
}

func (a *LargeBinary) GetOneForMarshal(i int) interface{} {
return a.GetOneForMarshalNullable(i, true)
}

func (a *LargeBinary) MarshalJSON() ([]byte, error) {
vals := make([]interface{}, a.Len())
for i := 0; i < a.Len(); i++ {
Expand Down Expand Up @@ -417,9 +425,9 @@ func (a *LargeBinary) ValidateFull() error {
return nil
}

func arrayEqualLargeBinary(left, right *LargeBinary) bool {
func arrayEqualLargeBinary(left, right *LargeBinary, opt equalOption) bool {
for i := 0; i < left.Len(); i++ {
if left.IsNull(i) {
if opt.nullable && left.IsNull(i) {
continue
}
if !bytes.Equal(left.Value(i), right.Value(i)) {
Expand Down Expand Up @@ -522,13 +530,17 @@ func (a *BinaryView) ValueStr(i int) string {
return base64.StdEncoding.EncodeToString(a.Value(i))
}

func (a *BinaryView) GetOneForMarshal(i int) interface{} {
if a.IsNull(i) {
func (a *BinaryView) GetOneForMarshalNullable(i int, nullable bool) interface{} {
if nullable && a.IsNull(i) {
return nil
}
return a.Value(i)
}

func (a *BinaryView) GetOneForMarshal(i int) interface{} {
return a.GetOneForMarshalNullable(i, true)
}

func (a *BinaryView) MarshalJSON() ([]byte, error) {
vals := make([]interface{}, a.Len())
for i := 0; i < a.Len(); i++ {
Expand All @@ -539,10 +551,10 @@ func (a *BinaryView) MarshalJSON() ([]byte, error) {
return json.Marshal(vals)
}

func arrayEqualBinaryView(left, right *BinaryView) bool {
func arrayEqualBinaryView(left, right *BinaryView, opt equalOption) bool {
leftBufs, rightBufs := left.dataBuffers, right.dataBuffers
for i := 0; i < left.Len(); i++ {
if left.IsNull(i) {
if opt.nullable && left.IsNull(i) {
continue
}
if !left.ValueHeader(i).Equals(leftBufs, right.ValueHeader(i), rightBufs) {
Expand Down
12 changes: 8 additions & 4 deletions arrow/array/boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ func (a *Boolean) setData(data *Data) {
}
}

func (a *Boolean) GetOneForMarshal(i int) interface{} {
if a.IsValid(i) {
func (a *Boolean) GetOneForMarshalNullable(i int, nullable bool) interface{} {
if !nullable || a.IsValid(i) {
return a.Value(i)
}
return nil
}

func (a *Boolean) GetOneForMarshal(i int) interface{} {
return a.GetOneForMarshalNullable(i, true)
}

func (a *Boolean) MarshalJSON() ([]byte, error) {
vals := make([]interface{}, a.Len())
for i := 0; i < a.Len(); i++ {
Expand All @@ -109,9 +113,9 @@ func (a *Boolean) MarshalJSON() ([]byte, error) {
return json.Marshal(vals)
}

func arrayEqualBoolean(left, right *Boolean) bool {
func arrayEqualBoolean(left, right *Boolean, opt equalOption) bool {
for i := 0; i < left.Len(); i++ {
if left.IsNull(i) {
if opt.nullable && left.IsNull(i) {
continue
}
if left.Value(i) != right.Value(i) {
Expand Down
Loading
Loading