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
12 changes: 12 additions & 0 deletions runtime/drivers/druid/druidsqldriver/druid_api_sql_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ type DruidQueryContext struct {
UseCache *bool `json:"useCache,omitempty"`
PopulateCache *bool `json:"populateCache,omitempty"`
Priority int `json:"priority,omitempty"`
UserEmail string `json:"rillUserEmail,omitempty"`
ServiceToken string `json:"rillServiceToken,omitempty"`
Comment on lines +461 to +462

@pjain1 pjain1 Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of adding custom keys here, I would rather pass in key/values from stmt.QueryAttributes as it is so any arbitrary attributes you define in metrics view will be passed as it in Druid context. But this would either need a custom marshaller for DruidQueryContext or changing the struct to just a map.

}

type DruidParameter struct {
Expand All @@ -482,13 +484,19 @@ func newDruidRequest(query string, args []driver.NamedValue, queryCfg *QueryConf
Value: arg.Value,
}
}

var useCache, populateCache *bool
priority := 0
userEmail := ""
serviceToken := ""
if queryCfg != nil {
useCache = queryCfg.UseCache
populateCache = queryCfg.PopulateCache
priority = queryCfg.Priority
userEmail = queryCfg.UserEmail
serviceToken = queryCfg.ServiceToken
}

return &DruidRequest{
Query: query,
Header: true,
Expand All @@ -501,6 +509,8 @@ func newDruidRequest(query string, args []driver.NamedValue, queryCfg *QueryConf
UseCache: useCache,
PopulateCache: populateCache,
Priority: priority,
UserEmail: userEmail,
ServiceToken: serviceToken,
},
}
}
Expand All @@ -517,6 +527,8 @@ type QueryConfig struct {
UseCache *bool
PopulateCache *bool
Priority int
UserEmail string
ServiceToken string
}

type queryCfgCtxKey struct{}
Expand Down
96 changes: 96 additions & 0 deletions runtime/drivers/druid/druidsqldriver/druid_api_sql_driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package druidsqldriver

import (
"context"
"database/sql"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

// newTestServer returns a Druid SQL API stub that captures each request body into requests,
// and responds with a minimal valid arrayLines result (header, types header, one row).
func newTestServer(t *testing.T, requests *[]DruidRequest) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

var dr DruidRequest
require.NoError(t, json.Unmarshal(body, &dr))
*requests = append(*requests, dr)

w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte("[\"n\"]\n[\"BIGINT\"]\n[1]\n"))
require.NoError(t, err)
}))
}

func TestQueryContext(t *testing.T) {
var requests []DruidRequest
srv := newTestServer(t, &requests)
defer srv.Close()

db, err := sql.Open("druid", srv.URL)
require.NoError(t, err)
defer db.Close()

useCache := true
ctx := WithQueryConfig(context.Background(), &QueryConfig{
UseCache: &useCache,
Priority: 3,
UserEmail: "user@example.com",
ServiceToken: "etl-bot",
})

rows, err := db.QueryContext(ctx, "SELECT 1")
require.NoError(t, err)
require.NoError(t, rows.Close())

require.Len(t, requests, 1)
qc := requests[0].Context
require.NotEmpty(t, qc.SQLQueryID)
require.True(t, qc.EnableTimeBoundaryPlanning)
require.NotNil(t, qc.UseCache)
require.True(t, *qc.UseCache)
require.Nil(t, qc.PopulateCache)
require.Equal(t, 3, qc.Priority)
require.Equal(t, "user@example.com", qc.UserEmail)
require.Equal(t, "etl-bot", qc.ServiceToken)
}

func TestQueryContextDefaults(t *testing.T) {
var requests []DruidRequest
srv := newTestServer(t, &requests)
defer srv.Close()

db, err := sql.Open("druid", srv.URL)
require.NoError(t, err)
defer db.Close()

rows, err := db.QueryContext(context.Background(), "SELECT 1")
require.NoError(t, err)
require.NoError(t, rows.Close())

require.Len(t, requests, 1)
qc := requests[0].Context
require.NotEmpty(t, qc.SQLQueryID)
require.Nil(t, qc.UseCache)
require.Nil(t, qc.PopulateCache)
require.Zero(t, qc.Priority)
require.Empty(t, qc.UserEmail)

// Fields with omitempty must not be serialized when unset.
b, err := json.Marshal(qc)
require.NoError(t, err)
var m map[string]any
require.NoError(t, json.Unmarshal(b, &m))
require.NotContains(t, m, "rillUserEmail")
require.NotContains(t, m, "rillServiceToken")
require.NotContains(t, m, "priority")
require.NotContains(t, m, "useCache")
}
14 changes: 14 additions & 0 deletions runtime/drivers/druid/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@ func (c *connection) Query(ctx context.Context, stmt *drivers.Statement) (res *d
}
queryCfg.Priority = stmt.Priority
}
// The metrics view's query_attributes can identify the caller under the "user_email" and "service_token" keys
// (e.g. user_email: '{{ .user.email }}'); they are stamped on the Druid query context for attribution in Druid's query logs.
if email := stmt.QueryAttributes["user_email"]; email != "" {
if queryCfg == nil {
queryCfg = &druidsqldriver.QueryConfig{}
}
queryCfg.UserEmail = email
}
if svc := stmt.QueryAttributes["service_token"]; svc != "" {
if queryCfg == nil {
queryCfg = &druidsqldriver.QueryConfig{}
}
queryCfg.ServiceToken = svc
}

if queryCfg != nil {
ctx = druidsqldriver.WithQueryConfig(ctx, queryCfg)
Expand Down
Loading