Skip to content

Commit cb1c46c

Browse files
nitinbhakarclaude
andauthored
Add StarRocks Arrow Flight SQL plugin with direct backend fetch (#120)
* Add StarRocks Arrow Flight SQL plugin with direct backend fetch Connects to StarRocks via Arrow Flight SQL and, when the FE returns distinct per-backend endpoint locations, redeems each ticket by dialing the BE directly and fanning the fetches out in parallel instead of funneling every row back through the FE. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix Wiz findings: bump vulnerable grpc dependency, pin minimum TLS version - google.golang.org/grpc was pulled in below v1.79.3, affected by CVE-2026-33186 - dialFlightClient's TLS config lacked an explicit MinVersion (CWE-327) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Bump transitive deps flagged by Wiz (otel, x/crypto, x/net) go.opentelemetry.io/otel < 1.41.0 (CVE-2026-29181), golang.org/x/crypto < 0.52.0 (13 CVEs), and golang.org/x/net < 0.55.0 (CVE-2026-25680, CVE-2026-39821) were pulled in transitively by the grpc bump. The crypto fix version requires go >= 1.25, hence the go.mod bump; GOTOOLCHAIN=auto (unpinned in this repo) lets CI's Go 1.24.5 fetch 1.25 automatically rather than needing a workflow change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Version bump * Split starrocks.go into one file per struct for readability command_context.go (commandContext + plugin.Handler methods), cluster_context.go (clusterContext, no methods), job_context.go (jobContext + collectResults), backend_client_pool.go (backendClientPool + fetchEndpoint/locationHost). starrocks.go keeps just the shared consts/telemetry vars and dialFlightClient, used by both command_context.go and backend_client_pool.go. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fold commandContext and clusterContext back into starrocks.go Keep job_context.go and backend_client_pool.go split out, but commandContext/clusterContext (plugin.Handler entry points + cluster config) stay in the main file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5aba4a3 commit cb1c46c

10 files changed

Lines changed: 783 additions & 50 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Setup Go
1313
uses: actions/setup-go@v5
1414
with:
15-
go-version: 1.24.5
15+
go-version: 1.25.0
1616
# setup-go caches the module + build cache keyed on go.sum by default.
1717
- name: Cache pnpm store
1818
uses: actions/cache@v4

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.24.6 AS go-builder
1+
FROM golang:1.25.0 AS go-builder
22

33
RUN apt-get update && apt-get install -y --no-install-recommends \
44
build-essential \

go.mod

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
module github.com/patterninc/heimdall
22

3-
go 1.24.6
3+
go 1.25.0
44

55
require (
66
github.com/ClickHouse/clickhouse-go/v2 v2.40.3
77
github.com/antlr4-go/antlr/v4 v4.13.1
8+
github.com/apache/arrow-go/v18 v18.4.0
89
github.com/aws/aws-sdk-go-v2 v1.42.0
910
github.com/aws/aws-sdk-go-v2/config v1.30.3
1011
github.com/aws/aws-sdk-go-v2/credentials v1.18.3
@@ -30,6 +31,8 @@ require (
3031
github.com/shopspring/decimal v1.4.0
3132
github.com/snowflakedb/gosnowflake v1.15.0
3233
github.com/stretchr/testify v1.11.1
34+
golang.org/x/sync v0.20.0
35+
google.golang.org/grpc v1.79.3
3336
gopkg.in/yaml.v3 v3.0.1
3437
k8s.io/api v0.33.4
3538
k8s.io/apimachinery v0.33.4
@@ -46,7 +49,6 @@ require (
4649
github.com/BurntSushi/toml v1.5.0 // indirect
4750
github.com/ClickHouse/ch-go v0.68.0 // indirect
4851
github.com/andybalholm/brotli v1.2.0 // indirect
49-
github.com/apache/arrow-go/v18 v18.4.0 // indirect
5052
github.com/apache/thrift v0.22.0 // indirect
5153
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
5254
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 // indirect
@@ -111,23 +113,24 @@ require (
111113
github.com/stretchr/objx v0.5.2 // indirect
112114
github.com/x448/float16 v0.8.4 // indirect
113115
github.com/zeebo/xxh3 v1.0.2 // indirect
114-
go.opentelemetry.io/otel v1.38.0 // indirect
115-
go.opentelemetry.io/otel/trace v1.38.0 // indirect
116+
go.opentelemetry.io/otel v1.41.0 // indirect
117+
go.opentelemetry.io/otel/trace v1.41.0 // indirect
116118
go.yaml.in/yaml/v2 v2.4.2 // indirect
117119
go.yaml.in/yaml/v3 v3.0.4 // indirect
118-
golang.org/x/crypto v0.42.0 // indirect
120+
golang.org/x/crypto v0.52.0 // indirect
119121
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
120-
golang.org/x/mod v0.27.0 // indirect
121-
golang.org/x/net v0.44.0 // indirect
122-
golang.org/x/oauth2 v0.30.0 // indirect
123-
golang.org/x/sync v0.17.0 // indirect
124-
golang.org/x/sys v0.36.0 // indirect
125-
golang.org/x/term v0.35.0 // indirect
126-
golang.org/x/text v0.29.0 // indirect
122+
golang.org/x/mod v0.35.0 // indirect
123+
golang.org/x/net v0.55.0 // indirect
124+
golang.org/x/oauth2 v0.34.0 // indirect
125+
golang.org/x/sys v0.45.0 // indirect
126+
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
127+
golang.org/x/term v0.43.0 // indirect
128+
golang.org/x/text v0.37.0 // indirect
127129
golang.org/x/time v0.11.0 // indirect
128-
golang.org/x/tools v0.36.0 // indirect
130+
golang.org/x/tools v0.44.0 // indirect
129131
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
130-
google.golang.org/protobuf v1.36.6 // indirect
132+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
133+
google.golang.org/protobuf v1.36.10 // indirect
131134
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
132135
gopkg.in/inf.v0 v0.9.1 // indirect
133136
gopkg.in/linkedin/goavro.v1 v1.0.5 // indirect

go.sum

Lines changed: 64 additions & 34 deletions
Large diffs are not rendered by default.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package starrocks
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/apache/arrow-go/v18/arrow"
8+
"github.com/apache/arrow-go/v18/arrow/array"
9+
10+
"github.com/patterninc/heimdall/pkg/result/column"
11+
)
12+
13+
var arrowTypeToResultTypeName = map[arrow.Type]string{
14+
arrow.INT8: "int",
15+
arrow.INT16: "int",
16+
arrow.INT32: "int",
17+
arrow.INT64: "long",
18+
arrow.UINT8: "int",
19+
arrow.UINT16: "int",
20+
arrow.UINT32: "long",
21+
arrow.UINT64: "long",
22+
arrow.FLOAT32: "float",
23+
arrow.FLOAT64: "double",
24+
arrow.DECIMAL128: "double",
25+
arrow.DECIMAL256: "double",
26+
arrow.STRING: "string",
27+
arrow.LARGE_STRING: "string",
28+
arrow.BINARY: "string",
29+
arrow.LARGE_BINARY: "string",
30+
arrow.BOOL: "boolean",
31+
arrow.DATE32: "string",
32+
arrow.DATE64: "string",
33+
arrow.TIMESTAMP: "string",
34+
arrow.TIME32: "string",
35+
arrow.TIME64: "string",
36+
arrow.LIST: "string",
37+
arrow.LARGE_LIST: "string",
38+
arrow.MAP: "string",
39+
arrow.STRUCT: "string",
40+
}
41+
42+
func columnsFromSchema(schema *arrow.Schema) []*column.Column {
43+
44+
fields := schema.Fields()
45+
columns := make([]*column.Column, len(fields))
46+
47+
for i, f := range fields {
48+
typeName, ok := arrowTypeToResultTypeName[f.Type.ID()]
49+
if !ok {
50+
typeName = "string"
51+
}
52+
columns[i] = &column.Column{
53+
Name: f.Name,
54+
Type: column.Type(typeName),
55+
}
56+
}
57+
58+
return columns
59+
60+
}
61+
62+
func recordToRows(rec arrow.Record) [][]any {
63+
64+
numRows := int(rec.NumRows())
65+
numCols := int(rec.NumCols())
66+
67+
rows := make([][]any, numRows)
68+
for r := 0; r < numRows; r++ {
69+
row := make([]any, numCols)
70+
for c := 0; c < numCols; c++ {
71+
row[c] = arrowValue(rec.Column(c), r)
72+
}
73+
rows[r] = row
74+
}
75+
76+
return rows
77+
78+
}
79+
80+
func arrowValue(col arrow.Array, i int) any {
81+
82+
if col.IsNull(i) {
83+
return nil
84+
}
85+
86+
switch v := col.(type) {
87+
case *array.Int8:
88+
return v.Value(i)
89+
case *array.Int16:
90+
return v.Value(i)
91+
case *array.Int32:
92+
return v.Value(i)
93+
case *array.Int64:
94+
return v.Value(i)
95+
case *array.Uint8:
96+
return v.Value(i)
97+
case *array.Uint16:
98+
return v.Value(i)
99+
case *array.Uint32:
100+
return v.Value(i)
101+
case *array.Uint64:
102+
return v.Value(i)
103+
case *array.Float32:
104+
return v.Value(i)
105+
case *array.Float64:
106+
return v.Value(i)
107+
case *array.Decimal128:
108+
return v.Value(i).ToFloat64(int32(v.DataType().(*arrow.Decimal128Type).Scale))
109+
case *array.Decimal256:
110+
return v.Value(i).ToFloat64(int32(v.DataType().(*arrow.Decimal256Type).Scale))
111+
case *array.String:
112+
return v.Value(i)
113+
case *array.LargeString:
114+
return v.Value(i)
115+
case *array.Binary:
116+
return string(v.Value(i))
117+
case *array.LargeBinary:
118+
return string(v.Value(i))
119+
case *array.Boolean:
120+
return v.Value(i)
121+
case *array.Date32:
122+
return v.Value(i).ToTime().Format("2006-01-02")
123+
case *array.Date64:
124+
return v.Value(i).ToTime().Format("2006-01-02")
125+
case *array.Timestamp:
126+
unit := v.DataType().(*arrow.TimestampType).Unit
127+
return v.Value(i).ToTime(unit).Format("2006-01-02T15:04:05.999999999")
128+
case *array.Time32:
129+
return v.Value(i).ToTime(v.DataType().(*arrow.Time32Type).Unit).Format("15:04:05")
130+
case *array.Time64:
131+
return v.Value(i).ToTime(v.DataType().(*arrow.Time64Type).Unit).Format("15:04:05.999999999")
132+
case *array.List, *array.LargeList, *array.Map, *array.Struct:
133+
b, err := json.Marshal(col.GetOneForMarshal(i))
134+
if err != nil {
135+
return fmt.Sprintf("%v", col.GetOneForMarshal(i))
136+
}
137+
return string(b)
138+
default:
139+
return fmt.Sprintf("%v", col.GetOneForMarshal(i))
140+
}
141+
142+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package starrocks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"sync"
8+
9+
"github.com/apache/arrow-go/v18/arrow/flight"
10+
"github.com/apache/arrow-go/v18/arrow/flight/flightsql"
11+
)
12+
13+
// backendClientPool lazily dials one Flight SQL client per unique BE
14+
// location, reusing the FE client when an endpoint has no distinct BE address.
15+
type backendClientPool struct {
16+
feClient *flightsql.Client
17+
dialedEndpoint string
18+
useTLS bool
19+
20+
mu sync.Mutex
21+
clients map[string]*flightsql.Client
22+
}
23+
24+
func newBackendClientPool(feClient *flightsql.Client, dialedEndpoint string, useTLS bool) *backendClientPool {
25+
return &backendClientPool{
26+
feClient: feClient,
27+
dialedEndpoint: dialedEndpoint,
28+
useTLS: useTLS,
29+
clients: make(map[string]*flightsql.Client),
30+
}
31+
}
32+
33+
// clientFor returns the FE client, or a cached/dialed direct BE connection if the endpoint has a distinct location.
34+
func (p *backendClientPool) clientFor(ctx context.Context, endpoint *flight.FlightEndpoint) (*flightsql.Client, error) {
35+
36+
for _, loc := range endpoint.Location {
37+
if loc.Uri == flight.LocationReuseConnection {
38+
continue
39+
}
40+
host := locationHost(loc.Uri)
41+
if host == `` {
42+
return nil, fmt.Errorf("failed to parse flight endpoint location %q", loc.Uri)
43+
}
44+
if host == p.dialedEndpoint {
45+
continue
46+
}
47+
return p.get(ctx, host)
48+
}
49+
50+
return p.feClient, nil
51+
52+
}
53+
54+
func (p *backendClientPool) get(ctx context.Context, host string) (*flightsql.Client, error) {
55+
56+
p.mu.Lock()
57+
defer p.mu.Unlock()
58+
59+
if c, ok := p.clients[host]; ok {
60+
return c, nil
61+
}
62+
63+
c, err := dialFlightClient(ctx, host, p.useTLS)
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to connect to StarRocks backend %q: %v", host, err)
66+
}
67+
68+
p.clients[host] = c
69+
return c, nil
70+
71+
}
72+
73+
func (p *backendClientPool) closeAll() {
74+
p.mu.Lock()
75+
defer p.mu.Unlock()
76+
for _, c := range p.clients {
77+
c.Client.Close()
78+
}
79+
}
80+
81+
func fetchEndpoint(ctx context.Context, beClients *backendClientPool, endpoint *flight.FlightEndpoint) ([][]any, error) {
82+
83+
client, err := beClients.clientFor(ctx, endpoint)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
reader, err := client.DoGet(ctx, endpoint.Ticket)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to fetch result stream: %v", err)
91+
}
92+
defer reader.Release()
93+
94+
rows := make([][]any, 0, 128)
95+
for reader.Next() {
96+
rows = append(rows, recordToRows(reader.Record())...)
97+
}
98+
99+
if err := reader.Err(); err != nil {
100+
return nil, fmt.Errorf("error reading result stream: %v", err)
101+
}
102+
103+
return rows, nil
104+
105+
}
106+
107+
// locationHost extracts host:port from a Flight location URI (e.g. "grpc+tcp://host:9408").
108+
func locationHost(uri string) string {
109+
u, err := url.Parse(uri)
110+
if err != nil {
111+
return ``
112+
}
113+
return u.Host
114+
}

0 commit comments

Comments
 (0)