serramatutu commented on code in PR #833:
URL: https://github.com/apache/arrow-go/pull/833#discussion_r3847193880


##########
arrow/array/struct_test.go:
##########
@@ -472,52 +474,48 @@ func TestStructArrayUnmarshalJSONMissingFields(t 
*testing.T) {
                name      string
                jsonInput string
                want      string
-               panic     bool
+               panicErr  error
        }{
                {
                        name:      "missing required field",
                        jsonInput: `[{"f2": 3, "f3": {"f3_1": "test"}}]`,
-                       panic:     true,
+                       panicErr:  errors.New("arrow/array: index out of 
range"),
                        want:      "",
                },
                {
                        name:      "missing optional fields",
                        jsonInput: `[{"f2": 3, "f3": {"f3_3": "test"}}]`,
-                       panic:     false,
+                       panicErr:  nil,
                        want:      `{[(null)] [3] {[(null)] [(null)] 
["test"]}}`,
                },
+               {
+                       name:      "explicit null in required field",
+                       jsonInput: `[{"f2": 3, "f3": {"f3_3": null}}]`,
+                       panicErr:  errors.New("field 'f3_3' is non-nullable but 
got null"),
+                       want:      "",
+               },
        }
 
        for _, tc := range tests {
                t.Run(
                        tc.name, func(t *testing.T) {
-
-                               var val bool
-
                                sb := array.NewStructBuilder(pool, dtype)
                                defer sb.Release()
 
-                               if tc.panic {
-                                       defer func() {
-                                               e := recover()
-                                               if e == nil {
-                                                       t.Fatalf("this should 
have panicked, but did not; slice value %v", val)
-                                               }
-                                               if got, want := e.(string), 
"arrow/array: index out of range"; got != want {
-                                                       t.Fatalf("invalid 
error. got=%q, want=%q", got, want)
-                                               }
-                                       }()
-                               } else {
-                                       defer func() {
-                                               if e := recover(); e != nil {
-                                                       t.Fatalf("unexpected 
panic: %v", e)
-                                               }
-                                       }()
-                               }
+                               defer func() {
+                                       e := recover()
+                                       if e == nil && tc.panicErr != nil {
+                                               t.Fatalf("did not panic, 
expected panic: %v", tc.panicErr)
+                                       } else if e != nil && tc.panicErr == 
nil {
+                                               t.Fatalf("unexpected panic: 
%v", e)
+                                       } else if e != nil && tc.panicErr != 
nil && fmt.Errorf("%s", e).Error() != tc.panicErr.Error() {
+                                               t.Fatalf("invalid error. 
got=%v, want=%v", e, tc.panicErr.Error())
+                                       }
+                               }()
 
                                err := sb.UnmarshalJSON([]byte(tc.jsonInput))
                                if err != nil {
-                                       t.Fatal(err)
+                                       panic(err)
                                }

Review Comment:
   Fixed. The test no longer converts errors into panics: it asserts the 
returned error with `require.ErrorContains` (and `main` has since rewritten 
this test to drop the panic-recover harness altogether).



##########
arrow/array/record.go:
##########
@@ -434,43 +433,69 @@ func (b *RecordBuilder) UnmarshalOne(dec *json.Decoder) 
error {
                return fmt.Errorf("record should start with '{', not %s", t)
        }
 
-       keylist := make(map[string]bool)
+       // consume one row checking for duplicates and nulls
+       keylist := make(map[string]json.RawMessage)
        for dec.More() {
                keyTok, err := dec.Token()
                if err != nil {
                        return err
                }
 
                key := keyTok.(string)
-               if keylist[key] {
+               if _, ok := keylist[key]; ok {
                        return fmt.Errorf("key %s shows up twice in row to be 
decoded", key)
                }
-               keylist[key] = true
+
+               var val json.RawMessage
+               if err := dec.Decode(&val); err != nil {
+                       return err
+               }
 
                indices := b.schema.FieldIndices(key)
                if len(indices) == 0 {
-                       var extra interface{}
-                       if err := dec.Decode(&extra); err != nil {
-                               return err
-                       }
                        continue
                }
 
-               if err := b.fields[indices[0]].UnmarshalOne(dec); err != nil {
-                       return err
+               idx := indices[0]
+
+               if bytes.Equal(val, []byte("null")) && 
!b.schema.Field(idx).Nullable {
+                       return fmt.Errorf("field '%s' is non-nullable but got 
null", key)
                }
+
+               keylist[key] = val
        }
 
        // consume the closing '}'
        if _, err := dec.Token(); err != nil {
                return err
        }
 
+       // check that all non-nullable fields were specified
+       for i := 0; i < b.schema.NumFields(); i++ {
+               f := b.schema.Field(i)
+               if _, ok := keylist[f.Name]; !ok && !f.Nullable {
+                       return fmt.Errorf("field '%s' is required but no value 
was given", f.Name)
+               }
+       }
+
+       // at this point we know there are no integrity errors, append values 
to field builders
+       for key, val := range keylist {
+               valDec := json.NewDecoder(bytes.NewReader(val))
+               valDec.UseNumber()
+
+               indices := b.schema.FieldIndices(key)
+               if err := b.fields[indices[0]].UnmarshalOne(valDec); err != nil 
{
+                       return err
+               }
+       }

Review Comment:
   Fixed. `unmarshalOne` validates the whole buffered row up front, and any 
error from a field builder is rolled back through the per-row 
`builderCheckpoint` added in #1113, so column lengths can no longer diverge.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to