diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a4ccd0..1db080fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Reduce kernel-backend JSON rendering allocations for nested Boolean and signed-integer values. - **Behavior change:** Native Arrow TIMESTAMP_NTZ results now preserve their wall-clock fields and report `TIMESTAMP_NTZ` on both backends. Returned `time.Time` values use UTC instead of the session location; their represented instant and `.UTC()` value are unchanged (databricks/databricks-sql-go#506). On Thrift, this requires a server-supplied Arrow schema with native timestamps; without it, NTZ still reports `TIMESTAMP` and uses session-location handling. String timestamps (`UseArrowNativeTimestamp=false`, internal) and non-Arrow values also retain legacy parsing. ## v1.18.0 (2026-10-24) diff --git a/internal/arrowscan/arrowscan.go b/internal/arrowscan/arrowscan.go index 8ff76aba..cbec7a1b 100644 --- a/internal/arrowscan/arrowscan.go +++ b/internal/arrowscan/arrowscan.go @@ -23,6 +23,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strconv" "strings" "time" @@ -433,6 +434,21 @@ func writeJSON(b *strings.Builder, col arrow.Array, row int, loc *time.Location, return nil } switch c := col.(type) { + case *array.Boolean: + if c.Value(row) { + b.WriteString("true") + } else { + b.WriteString("false") + } + return nil + case *array.Int8: + return writeIntJSON(b, int64(c.Value(row))) + case *array.Int16: + return writeIntJSON(b, int64(c.Value(row))) + case *array.Int32: + return writeIntJSON(b, int64(c.Value(row))) + case *array.Int64: + return writeIntJSON(b, c.Value(row)) case *array.List: // Use the offset-aware ValueOffsets, NOT Offsets()[row]: arrow-go's // Offsets() returns the full un-sliced buffer, so a List with a non-zero @@ -479,6 +495,12 @@ func writeJSON(b *strings.Builder, col arrow.Array, row int, loc *time.Location, } } +func writeIntJSON(b *strings.Builder, v int64) error { + var scratch [20]byte // Sign and decimal digits of math.MinInt64 fit. + b.Write(strconv.AppendInt(scratch[:0], v, 10)) + return nil +} + func writeListJSON(b *strings.Builder, values arrow.Array, start, end int, loc *time.Location, keys *StructKeyCache) error { b.WriteByte('[') for i := start; i < end; i++ { diff --git a/internal/rows/arrowbased/arrowscan_parity_test.go b/internal/rows/arrowbased/arrowscan_parity_test.go index aeaaab30..495fde95 100644 --- a/internal/rows/arrowbased/arrowscan_parity_test.go +++ b/internal/rows/arrowbased/arrowscan_parity_test.go @@ -1,6 +1,7 @@ package arrowbased import ( + "strings" "testing" "time" @@ -253,6 +254,30 @@ func TestArrowbasedKernelRenderParity(t *testing.T) { name string build func() arrow.Array }{ + {"sliced_struct_scalar_leaves", func() arrow.Array { + dt := arrow.StructOf( + arrow.Field{Name: "flag", Type: arrow.FixedWidthTypes.Boolean, Nullable: true}, + arrow.Field{Name: "tiny", Type: arrow.PrimitiveTypes.Int8, Nullable: true}, + arrow.Field{Name: "small", Type: arrow.PrimitiveTypes.Int16, Nullable: true}, + arrow.Field{Name: "int", Type: arrow.PrimitiveTypes.Int32, Nullable: true}, + arrow.Field{Name: "big", Type: arrow.PrimitiveTypes.Int64, Nullable: true}, + ) + input := `[ + {"flag":true,"tiny":1,"small":2,"int":3,"big":4}, + {"flag":true,"tiny":-128,"small":-32768,"int":-2147483648,"big":-9223372036854775808}, + {"flag":false,"tiny":127,"small":32767,"int":2147483647,"big":9223372036854775807}, + {"flag":true,"tiny":-1,"small":-1,"int":-1,"big":-1}, + {"flag":false,"tiny":0,"small":0,"int":0,"big":0}, + {"flag":null,"tiny":null,"small":null,"int":null,"big":null}, + null + ]` + full, _, err := array.FromJSON(pool, dt, strings.NewReader(input), array.WithUseNumber()) + if err != nil { + t.Fatal(err) + } + defer full.Release() + return array.NewSlice(full, 1, int64(full.Len())) + }}, {"sliced_list", func() arrow.Array { b := array.NewListBuilder(pool, arrow.PrimitiveTypes.Int64) vb := b.ValueBuilder().(*array.Int64Builder)