Skip to content

Commit cdfbeda

Browse files
authored
Merge pull request #32 from grafana/use-context
Bug fixes / context refactor
2 parents 3ff608c + d8ad3af commit cdfbeda

File tree

9 files changed

+79
-32
lines changed

9 files changed

+79
-32
lines changed

completion.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package sqlds
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89

910
"github.com/grafana/grafana-plugin-sdk-go/backend"
10-
"github.com/pkg/errors"
1111
)
1212

13+
// ErrorNotImplemented is returned if the function is not implemented by the provided Driver (the Completable pointer is nil)
14+
var ErrorNotImplemented = errors.New("not implemented")
15+
1316
// Completable will be used to autocomplete Tables Schemas and Columns for SQL languages
1417
type Completable interface {
1518
Schemas(ctx context.Context) ([]string, error)
@@ -43,7 +46,7 @@ type columnRequest struct {
4346

4447
func (ds *sqldatasource) getSchemas(rw http.ResponseWriter, req *http.Request) {
4548
if ds.Completable == nil {
46-
handleError(rw, errors.New("not implemented"))
49+
handleError(rw, ErrorNotImplemented)
4750
return
4851
}
4952

@@ -58,7 +61,7 @@ func (ds *sqldatasource) getSchemas(rw http.ResponseWriter, req *http.Request) {
5861

5962
func (ds *sqldatasource) getTables(rw http.ResponseWriter, req *http.Request) {
6063
if ds.Completable == nil {
61-
handleError(rw, errors.New("not implemented"))
64+
handleError(rw, ErrorNotImplemented)
6265
return
6366
}
6467

@@ -78,7 +81,7 @@ func (ds *sqldatasource) getTables(rw http.ResponseWriter, req *http.Request) {
7881

7982
func (ds *sqldatasource) getColumns(rw http.ResponseWriter, req *http.Request) {
8083
if ds.Completable == nil {
81-
handleError(rw, errors.New("not implemented"))
84+
handleError(rw, ErrorNotImplemented)
8285
return
8386
}
8487

datasource.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ package sqlds
33
import (
44
"context"
55
"database/sql"
6+
"errors"
7+
"fmt"
68
"net/http"
79
"sync"
810

911
"github.com/grafana/grafana-plugin-sdk-go/backend"
1012
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
1113
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
1214
"github.com/grafana/grafana-plugin-sdk-go/data"
13-
"github.com/pkg/errors"
1415
)
1516

1617
type sqldatasource struct {
17-
db *sql.DB
18-
c Driver
19-
settings backend.DataSourceInstanceSettings
18+
Completable
19+
20+
db *sql.DB
21+
c Driver
22+
23+
driverSettings DriverSettings
24+
settings backend.DataSourceInstanceSettings
2025

2126
backend.CallResourceHandler
22-
Completable
2327
CustomRoutes map[string]func(http.ResponseWriter, *http.Request)
2428
}
2529

@@ -37,7 +41,9 @@ func (ds *sqldatasource) NewDatasource(settings backend.DataSourceInstanceSettin
3741
if err != nil {
3842
return nil, err
3943
}
44+
4045
ds.CallResourceHandler = httpadapter.New(mux)
46+
ds.driverSettings = ds.c.Settings(settings)
4147

4248
return ds, nil
4349
}
@@ -66,7 +72,7 @@ func (ds *sqldatasource) QueryData(ctx context.Context, req *backend.QueryDataRe
6672
// Execute each query and store the results by query RefID
6773
for _, q := range req.Queries {
6874
go func(query backend.DataQuery) {
69-
frames, err := ds.handleQuery(query)
75+
frames, err := ds.handleQuery(ctx, query)
7076

7177
response.Set(query.RefID, backend.DataResponse{
7278
Frames: frames,
@@ -83,7 +89,7 @@ func (ds *sqldatasource) QueryData(ctx context.Context, req *backend.QueryDataRe
8389
}
8490

8591
// handleQuery will call query, and attempt to reconnect if the query failed
86-
func (ds *sqldatasource) handleQuery(req backend.DataQuery) (data.Frames, error) {
92+
func (ds *sqldatasource) handleQuery(ctx context.Context, req backend.DataQuery) (data.Frames, error) {
8793
// Convert the backend.DataQuery into a Query object
8894
q, err := GetQuery(req)
8995
if err != nil {
@@ -93,30 +99,42 @@ func (ds *sqldatasource) handleQuery(req backend.DataQuery) (data.Frames, error)
9399
// Apply supported macros to the query
94100
q.RawSQL, err = interpolate(ds.c, q)
95101
if err != nil {
96-
return nil, errors.WithMessage(err, "Could not apply macros")
102+
return nil, fmt.Errorf("%s: %w", "Could not apply macros", err)
97103
}
98104

99105
// Apply the default FillMode, overwritting it if the query specifies it
100-
fillMode := ds.c.FillMode()
106+
fillMode := ds.driverSettings.FillMode
101107
if q.FillMissing != nil {
102108
fillMode = q.FillMissing
103109
}
104110

111+
if ds.driverSettings.Timeout != 0 {
112+
tctx, cancel := context.WithTimeout(ctx, ds.driverSettings.Timeout)
113+
defer cancel()
114+
115+
ctx = tctx
116+
}
117+
105118
// FIXES:
106119
// * Some datasources (snowflake) expire connections or have an authentication token that expires if not used in 1 or 4 hours.
107120
// Because the datasource driver does not include an option for permanent connections, we retry the connection
108121
// if the query fails. NOTE: this does not include some errors like "ErrNoRows"
109-
res, err := query(ds.db, ds.c.Converters(), fillMode, q)
122+
res, err := query(ctx, ds.db, ds.c.Converters(), fillMode, q)
110123
if err == nil {
111124
return res, nil
112125
}
113126

114-
if errors.Cause(err) == ErrorQuery {
127+
if errors.Is(err, ErrorNoResults) {
128+
return res, nil
129+
}
130+
131+
if errors.Is(err, ErrorQuery) {
115132
ds.db, err = ds.c.Connect(ds.settings)
116133
if err != nil {
117134
return nil, err
118135
}
119-
return query(ds.db, ds.c.Converters(), fillMode, q)
136+
137+
return query(ctx, ds.db, ds.c.Converters(), fillMode, q)
120138
}
121139

122140
return nil, err

driver.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ package sqlds
22

33
import (
44
"database/sql"
5+
"time"
56

67
"github.com/grafana/grafana-plugin-sdk-go/backend"
78
"github.com/grafana/grafana-plugin-sdk-go/data"
89
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
910
)
1011

12+
type DriverSettings struct {
13+
Timeout time.Duration
14+
FillMode *data.FillMissing
15+
}
16+
1117
// Driver is a simple interface that defines how to connect to a backend SQL datasource
1218
// Plugin creators will need to implement this in order to create a managed datasource
1319
type Driver interface {
1420
// Connect connects to the database. It does not need to call `db.Ping()`
1521
Connect(backend.DataSourceInstanceSettings) (*sql.DB, error)
16-
FillMode() *data.FillMissing
22+
// Settings are read whenever the plugin is initialized, or after the data source settings are updated
23+
Settings(backend.DataSourceInstanceSettings) DriverSettings
1724
Macros() Macros
1825
Converters() []sqlutil.Converter
1926
}

errors.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package sqlds
22

3-
import "github.com/pkg/errors"
3+
import "errors"
44

55
var (
6-
// ErrorBadDatasource ...
6+
// ErrorBadDatasource is returned if the data source could not be asserted to the correct type (this should basically never happen?)
77
ErrorBadDatasource = errors.New("type assertion to datasource failed")
88
// ErrorJSON is returned when json.Unmarshal fails
99
ErrorJSON = errors.New("error unmarshaling query JSON the Query Model")
1010
// ErrorQuery is returned when the query could not complete / execute
1111
ErrorQuery = errors.New("error querying the database")
12+
// ErrorTimeout is returned if the query has timed out
13+
ErrorTimeout = errors.New("query timeout exceeded")
14+
// ErrorNoResults is returned if there were no results returned
15+
ErrorNoResults = errors.New("no results returned from query")
1216
)

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@ go 1.15
44

55
require (
66
github.com/grafana/grafana-plugin-sdk-go v0.94.0
7-
github.com/pkg/errors v0.9.1
87
github.com/stretchr/testify v1.7.0
98
)

go.sum

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
251251
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
252252
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
253253
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
254-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
255254
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
256255
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
257256
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

macros.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package sqlds
22

33
import (
4+
"errors"
45
"fmt"
56
"regexp"
67
"strings"
7-
8-
"github.com/pkg/errors"
98
)
109

1110
var (

macros_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"database/sql"
55
"fmt"
66
"testing"
7+
"time"
78

89
"github.com/grafana/grafana-plugin-sdk-go/backend"
9-
"github.com/grafana/grafana-plugin-sdk-go/data"
1010
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
@@ -17,12 +17,15 @@ type MockDB struct{}
1717
func (h *MockDB) Connect(backend.DataSourceInstanceSettings) (db *sql.DB, err error) {
1818
return
1919
}
20-
func (h *MockDB) FillMode() (mode *data.FillMissing) {
20+
21+
func (h *MockDB) Settings(backend.DataSourceInstanceSettings) (settings DriverSettings) {
2122
return
2223
}
24+
2325
func (h *MockDB) Converters() (sc []sqlutil.Converter) {
2426
return
2527
}
28+
2629
func (h *MockDB) Macros() (macros Macros) {
2730
return map[string]MacroFunc{
2831
"foo": func(query *Query, args []string) (out string, err error) {
@@ -37,6 +40,10 @@ func (h *MockDB) Macros() (macros Macros) {
3740
}
3841
}
3942

43+
func (h *MockDB) Timeout(backend.DataSourceInstanceSettings) time.Duration {
44+
return time.Minute
45+
}
46+
4047
func TestInterpolate(t *testing.T) {
4148
type test struct {
4249
name string

query.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package sqlds
22

33
import (
4+
"context"
45
"database/sql"
56
"encoding/json"
7+
"fmt"
68
"time"
79

810
"github.com/grafana/grafana-plugin-sdk-go/backend"
911
"github.com/grafana/grafana-plugin-sdk-go/data"
1012
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
11-
"github.com/pkg/errors"
1213
)
1314

1415
// FormatQueryOption defines how the user has chosen to represent the data
@@ -91,21 +92,21 @@ func getErrorFrameFromQuery(query *Query) data.Frames {
9192
}
9293

9394
// query sends the query to the sql.DB and converts the rows to a dataframe.
94-
func query(db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
95+
func query(ctx context.Context, db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissing, query *Query) (data.Frames, error) {
9596
// Query the rows from the database
96-
rows, err := db.Query(query.RawSQL)
97+
rows, err := db.QueryContext(ctx, query.RawSQL)
9798
if err != nil {
98-
return getErrorFrameFromQuery(query), errors.Wrap(ErrorQuery, err.Error())
99+
return getErrorFrameFromQuery(query), fmt.Errorf("%w: %s", ErrorQuery, err.Error())
99100
}
100101

101102
// Check for an error response
102103
if err := rows.Err(); err != nil {
103104
if err == sql.ErrNoRows {
104105
// Should we even response with an error here?
105106
// The panel will simply show "no data"
106-
return getErrorFrameFromQuery(query), errors.WithMessage(err, "No results from query")
107+
return getErrorFrameFromQuery(query), fmt.Errorf("%s: %w", "No results from query", err)
107108
}
108-
return getErrorFrameFromQuery(query), errors.WithMessage(err, "Error response from database")
109+
return getErrorFrameFromQuery(query), fmt.Errorf("%s: %w", "Error response from database", err)
109110
}
110111

111112
defer func() {
@@ -117,7 +118,7 @@ func query(db *sql.DB, converters []sqlutil.Converter, fillMode *data.FillMissin
117118
// Convert the response to frames
118119
res, err := getFrames(rows, -1, converters, fillMode, query)
119120
if err != nil {
120-
return nil, errors.WithMessage(err, "Could not process SQL results")
121+
return getErrorFrameFromQuery(query), fmt.Errorf("%w: %s", err, "Could not process SQL results")
121122
}
122123

123124
return res, nil
@@ -139,6 +140,16 @@ func getFrames(rows *sql.Rows, limit int64, converters []sqlutil.Converter, fill
139140
return data.Frames{frame}, nil
140141
}
141142

143+
count, err := frame.RowLen()
144+
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
if count == 0 {
150+
return nil, ErrorNoResults
151+
}
152+
142153
if frame.TimeSeriesSchema().Type == data.TimeSeriesTypeLong {
143154
frame, err := data.LongToWide(frame, fillMode)
144155
if err != nil {

0 commit comments

Comments
 (0)