Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release History

## Unreleased
- Fix a data race when timestamp results are read from concurrent queries: Arrow v12's `TimestampType.GetToTimeFunc` lazily caches the type's `*time.Location` without synchronization, and the driver calls it on the shared `arrow.FixedWidthTypes` singletons. The cache is now warmed at package init so later calls are read-only (databricks/databricks-sql-go#179)

## v1.15.0 (2026-08-28)
- **New experimental SEA/kernel backend (opt-in).** Set `WithUseKernel(true)` (or `useKernel=true` in the DSN) to route execution through the Statement Execution API instead of Thrift, backed by the Rust `databricks-sql-kernel` over cgo. It requires a build with `-tags databricks_kernel` and `CGO_ENABLED=1`; the default Thrift build is unchanged and returns a clear error if the kernel backend is selected without the tag. The prebuilt kernel binaries ship as per-platform Go modules, so `go get` pulls the one for your platform automatically — **no Rust toolchain or build step** — across 7 platforms (linux amd64/arm64/arm, darwin amd64/arm64, windows amd64/arm64). Kernel-backend features in this release: mTLS client certificates (`WithKernelClientCertificate`), identity federation (`WithFederatedTokenProvider*`), U2M on-disk token cache (`WithTokenCache`), configurable request timeout, opt-in lossy float64 decimals (`WithKernelDecimalAsFloat`), `GetArrowBatches` on the public `Rows` interface, kernel logs routed through the driver logger, cause-categorized telemetry errors, and an Azure U2M OAuth fix (databricks/databricks-sql-go#393, #399, #412, #440).
- Bump `golang.org/x/mod` to v0.40.0 to clear CVE-2026-56864 / CVE-2026-56865 (databricks/databricks-sql-go#460)
Expand Down
17 changes: 17 additions & 0 deletions internal/rows/arrowbased/arrowRows.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ type SparkArrowRecord interface {
arrow.Record
}

// Arrow v12's TimestampType caches its *time.Location lazily on the first
// GetZone/GetToTimeFunc call without synchronization (apache/arrow#38795,
// fixed in later Arrow versions). NewArrowRowScanner calls GetToTimeFunc on
// the shared arrow.FixedWidthTypes singletons, so concurrent queries race on
// that first call. Warming the cache here, before any concurrency is
// possible, makes every later call a plain read.
func init() {
for _, dt := range []arrow.DataType{
arrow.FixedWidthTypes.Timestamp_s,
arrow.FixedWidthTypes.Timestamp_ms,
arrow.FixedWidthTypes.Timestamp_us,
arrow.FixedWidthTypes.Timestamp_ns,
} {
_, _ = dt.(*arrow.TimestampType).GetToTimeFunc()
}
}

type timeStampFn func(arrow.Timestamp) time.Time

type colInfo struct {
Expand Down
33 changes: 33 additions & 0 deletions internal/rows/arrowbased/arrowRows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"math/big"
"os"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -2398,3 +2399,35 @@ func TestDecimalInComplexTypes(t *testing.T) {
assert.Equal(t, `{"col2":null}`, v)
})
}

// Regression test for databricks/databricks-sql-go#179: Arrow v12's
// TimestampType.GetToTimeFunc lazily caches the type's *time.Location without
// synchronization, so the first concurrent calls on the shared
// arrow.FixedWidthTypes timestamp singletons were a data race. The package
// init() in arrowRows.go warms that cache; without it, this test fails under
// the race detector.
func TestSharedTimestampGetToTimeFuncConcurrency(t *testing.T) {
sharedTimestampTypes := []arrow.DataType{
arrow.FixedWidthTypes.Timestamp_s,
arrow.FixedWidthTypes.Timestamp_ms,
arrow.FixedWidthTypes.Timestamp_us,
arrow.FixedWidthTypes.Timestamp_ns,
}

var wg sync.WaitGroup
for i := 0; i < 32; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for _, dt := range sharedTimestampTypes {
toTime, err := dt.(*arrow.TimestampType).GetToTimeFunc()
if err != nil {
t.Errorf("GetToTimeFunc failed for %s: %v", dt, err)
return
}
_ = toTime(arrow.Timestamp(0))
}
}()
}
wg.Wait()
}