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 85916dd8 fix(arrow/compute): allow casting nullable struct fields to
non-nullable when no nulls (#1363)
85916dd8 is described below
commit 85916dd841e44a7c982d73877064a08c518e9379
Author: Sankalp Thakur <[email protected]>
AuthorDate: Tue Oct 13 18:34:03 2026 +0400
fix(arrow/compute): allow casting nullable struct fields to non-nullable
when no nulls (#1363)
### Rationale for this change
Casting a struct with a nullable field to a struct type where that field
is non-nullable always failed with `arrow.ErrType`, even when the field
held no nulls. The C++ implementation only rejects the cast when the
field actually has nulls (`scalar_cast_nested.cc`).
### What changes are included in this PR?
`CastStruct` no longer rejects nullable to non-nullable fields while
matching field names. It checks each selected field's null count at cast
time instead, and returns `arrow.ErrInvalid` with the same message as
C++ when the field has nulls.
### Are these changes tested?
Yes. `TestStructToDifferentNullabilityStruct` now covers a field with no
nulls (cast succeeds), fields with nulls (`ErrInvalid`), and a slice
that skips the null rows (cast succeeds).
- `go test ./arrow/compute/...`: ok
- `go vet ./arrow/compute/...` and `gofmt -l arrow/compute`: clean
### Are there any user-facing changes?
Yes. These casts now succeed when the field has no nulls. When it has
nulls, the error is now `arrow.ErrInvalid` instead of `arrow.ErrType`,
matching C++.
Fixes #133
### AI/LLM disclosure
- AI coding tools (Claude Code) were used to write this change, the test
updates and this description.
- The change was reviewed and the commands above were run locally before
submitting.
---
arrow/compute/cast.go | 12 +++++++-----
arrow/compute/cast_test.go | 22 +++++++++++++++-------
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/arrow/compute/cast.go b/arrow/compute/cast.go
index eb3e8526..e1b504c0 100644
--- a/arrow/compute/cast.go
+++ b/arrow/compute/cast.go
@@ -650,10 +650,6 @@ func CastStruct(ctx *exec.KernelCtx, batch *exec.ExecSpan,
out *exec.ExecResult)
inField := inType.Field(inFieldIndex)
outField := outType.Field(outFieldIndex)
if inField.Name == outField.Name {
- if inField.Nullable && !outField.Nullable {
- return fmt.Errorf("%w: cannot cast nullable
field to non-nullable field: %s %s",
- arrow.ErrType, inType, outType)
- }
fieldsToSelect[outFieldIndex] = inFieldIndex
outFieldIndex++
}
@@ -678,7 +674,13 @@ func CastStruct(ctx *exec.KernelCtx, batch *exec.ExecSpan,
out *exec.ExecResult)
values = array.NewSlice(values, input.Offset, input.Len)
defer values.Release()
- opts.ToType = outType.Field(outFieldIndex).Type
+ inField, outField := inType.Field(idx),
outType.Field(outFieldIndex)
+ if inField.Nullable && !outField.Nullable && values.NullN() > 0
{
+ return fmt.Errorf("%w: field '%s' of type %s has nulls.
Can't cast to non-nullable field '%s' of type %s",
+ arrow.ErrInvalid, inField.Name, inField.Type,
outField.Name, outField.Type)
+ }
+
+ opts.ToType = outField.Type
castedValues, err := CastArray(ctx.Ctx, values, &opts)
if err != nil {
return err
diff --git a/arrow/compute/cast_test.go b/arrow/compute/cast_test.go
index 815c3275..f514582c 100644
--- a/arrow/compute/cast_test.go
+++ b/arrow/compute/cast_test.go
@@ -3761,8 +3761,8 @@ func (c *CastSuite)
TestStructToDifferentNullabilityStruct() {
dest1NonNullable := arrow.StructOf(fieldsDest1NonNullable...)
options1NoNullable := compute.SafeCastOptions(dest1NonNullable)
_, err = compute.CastArray(context.TODO(), srcNullable,
options1NoNullable)
- c.ErrorIs(err, arrow.ErrType)
- c.ErrorContains(err, "cannot cast nullable field to
non-nullable field")
+ c.ErrorIs(err, arrow.ErrInvalid)
+ c.ErrorContains(err, "has nulls")
fieldsDest2NonNullable := []arrow.Field{
{Name: "a", Type: arrow.PrimitiveTypes.Int64, Nullable:
false},
@@ -3771,17 +3771,25 @@ func (c *CastSuite)
TestStructToDifferentNullabilityStruct() {
dest2NonNullable := arrow.StructOf(fieldsDest2NonNullable...)
options2NoNullable := compute.SafeCastOptions(dest2NonNullable)
_, err = compute.CastArray(context.TODO(), srcNullable,
options2NoNullable)
- c.ErrorIs(err, arrow.ErrType)
- c.ErrorContains(err, "cannot cast nullable field to
non-nullable field")
+ c.ErrorIs(err, arrow.ErrInvalid)
+ c.ErrorContains(err, "has nulls")
fieldsDest3NonNullable := []arrow.Field{
{Name: "c", Type: arrow.PrimitiveTypes.Int64, Nullable:
false},
}
dest3NonNullable := arrow.StructOf(fieldsDest3NonNullable...)
options3NoNullable := compute.SafeCastOptions(dest3NonNullable)
- _, err = compute.CastArray(context.TODO(), srcNullable,
options3NoNullable)
- c.ErrorIs(err, arrow.ErrType)
- c.ErrorContains(err, "cannot cast nullable field to
non-nullable field")
+ out3, err := compute.CastArray(context.TODO(), srcNullable,
options3NoNullable)
+ c.NoError(err) // column c has no nulls
+ defer out3.Release()
+ c.Equal(3, out3.Len())
+
+ // slice that skips the nulls in a and b succeeds
+ sliced := array.NewSlice(srcNullable, 0, 1)
+ defer sliced.Release()
+ out4, err := compute.CastArray(context.TODO(), sliced,
options1NoNullable)
+ c.NoError(err)
+ defer out4.Release()
})
}