From fd1175a70ca1989a6dc15722467e69e7127cca8d Mon Sep 17 00:00:00 2001 From: vulragrag-star Date: Mon, 28 Sep 2026 08:54:04 +0800 Subject: [PATCH] jsonio: encode nested none as null Top-level Write already skips none, and record fields omit none, but array/map/error paths called writePrimitive on TypeNone and panicked (unsupported id=30). Emit JSON null for nested none, matching arrowio. Fixes #7314 --- sio/jsonio/none_nested_test.go | 63 ++++++++++++++++++++++++++++++++++ sio/jsonio/writer.go | 5 +++ sio/jsonio/ztests/none.yaml | 6 ++++ 3 files changed, 74 insertions(+) create mode 100644 sio/jsonio/none_nested_test.go diff --git a/sio/jsonio/none_nested_test.go b/sio/jsonio/none_nested_test.go new file mode 100644 index 000000000..d39e704d4 --- /dev/null +++ b/sio/jsonio/none_nested_test.go @@ -0,0 +1,63 @@ +package jsonio + +import ( + "bytes" + "strings" + "testing" + + "github.com/brimdata/super" + "github.com/brimdata/super/sio" + "github.com/brimdata/super/sup" +) + +func TestNestedNoneJSON(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"none", ""}, + {"[none]", "[null]\n"}, + {"{a:[none]}", `{"a":[null]}` + "\n"}, + {"error(none)", `{"error":null}` + "\n"}, + {"{a:1,b?:none::int64}", `{"a":1}` + "\n"}, + } + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + val, err := sup.ParseValue(super.NewContext(), tc.in) + if err != nil { + t.Fatalf("parse: %v", err) + } + var buf bytes.Buffer + w := NewWriter(sio.NopCloser(&buf), WriterOpts{}) + if err := w.Write(val); err != nil { + t.Fatalf("write: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("close: %v", err) + } + got := buf.String() + if got != tc.want { + t.Fatalf("got %q want %q", got, tc.want) + } + }) + } +} + +func TestNestedNoneNoPanic(t *testing.T) { + for _, in := range []string{"[none]", "{a:[none]}", "error(none)", "values none | values len(this)"} { + // last case is spq; skip — covered by CLI. Keep value-level only. + if strings.Contains(in, "|") { + continue + } + val, err := sup.ParseValue(super.NewContext(), in) + if err != nil { + t.Fatalf("parse %s: %v", in, err) + } + var buf bytes.Buffer + w := NewWriter(sio.NopCloser(&buf), WriterOpts{}) + if err := w.Write(val); err != nil { + t.Fatalf("write %s: %v", in, err) + } + _ = w.Close() + } +} diff --git a/sio/jsonio/writer.go b/sio/jsonio/writer.go index 701024b06..5720f2bc7 100644 --- a/sio/jsonio/writer.go +++ b/sio/jsonio/writer.go @@ -70,6 +70,11 @@ func (w *Writer) Write(val super.Value) error { } func (w *Writer) writeAny(tab int, val super.Value) { + // Nested none: emit null (arrowio does the same). Top-level Write still skips none. + if val.IsNone() { + w.writeColor([]byte("null"), nullColor) + return + } val = val.Under() if val.IsNull() { w.writeColor([]byte("null"), nullColor) diff --git a/sio/jsonio/ztests/none.yaml b/sio/jsonio/ztests/none.yaml index 1f306e665..c4c4c5202 100644 --- a/sio/jsonio/ztests/none.yaml +++ b/sio/jsonio/ztests/none.yaml @@ -7,7 +7,13 @@ input: | {a:3,b?:none::int64} none none::int64 + [none] + {a:[none]} + error(none) output: | {"a":1,"b":2} {"a":3} + [null] + {"a":[null]} + {"error":null}