Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions internal/arrowscan/arrowscan.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low — The PR's main claim is fewer allocations, but no test or benchmark checks it. The parity case only shows the output still matches Thrift, so it would keep passing if a later edit put Boolean/int leaves back on scalarForJSON + json.Marshal. Consider adding a small testing.AllocsPerRun test or a Benchmark* in internal/arrowscan that renders an ARRAY<INT>/STRUCT<bool,int64> cell, so the allocation drop is measured and protected.

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++ {
Expand Down
25 changes: 25 additions & 0 deletions internal/rows/arrowbased/arrowscan_parity_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package arrowbased

import (
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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)
Expand Down
Loading