Skip to content

Commit 36b12cd

Browse files
Implementing error types (#117)
Updated driver error handling to standardize on three error types: driver error (DBDriverError), request error (DBRequestError), and sql execution error (DBExecutionError). All three types have accessors for stack trace, correlation id, and connection id. DBExecutionError also has accessors for query Id and sql state. The example in examples/error shows how to use errors.Is() and errors.As() to work with the error chain to determine error type and extract the state. --------- Signed-off-by: Matthew Kim <11141331+mattdeekay@users.noreply.github.com> Signed-off-by: Raymond Cypher <raymond.cypher@databricks.com> Co-authored-by: Matthew Kim <11141331+mattdeekay@users.noreply.github.com>
1 parent c0979e0 commit 36b12cd

27 files changed

Lines changed: 1543 additions & 774 deletions

connection.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import (
66
"time"
77

88
"github.com/databricks/databricks-sql-go/driverctx"
9+
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
910
"github.com/databricks/databricks-sql-go/internal/cli_service"
1011
"github.com/databricks/databricks-sql-go/internal/client"
1112
"github.com/databricks/databricks-sql-go/internal/config"
12-
dbsqlerr "github.com/databricks/databricks-sql-go/internal/err"
13+
dbsqlerrint "github.com/databricks/databricks-sql-go/internal/errors"
1314
"github.com/databricks/databricks-sql-go/internal/rows"
1415
"github.com/databricks/databricks-sql-go/internal/sentinel"
1516
"github.com/databricks/databricks-sql-go/logger"
@@ -46,19 +47,19 @@ func (c *conn) Close() error {
4647

4748
if err != nil {
4849
log.Err(err).Msg("databricks: failed to close connection")
49-
return dbsqlerr.WrapErr(err, "failed to close connection")
50+
return dbsqlerrint.NewRequestError(ctx, dbsqlerr.ErrCloseConnection, err)
5051
}
5152
return nil
5253
}
5354

5455
// Not supported in Databricks.
5556
func (c *conn) Begin() (driver.Tx, error) {
56-
return nil, errors.New(dbsqlerr.ErrTransactionsNotSupported)
57+
return nil, dbsqlerrint.NewDriverError(context.TODO(), dbsqlerr.ErrNotImplemented, nil)
5758
}
5859

5960
// Not supported in Databricks.
6061
func (c *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
61-
return nil, errors.New(dbsqlerr.ErrTransactionsNotSupported)
62+
return nil, dbsqlerrint.NewDriverError(context.TODO(), dbsqlerr.ErrNotImplemented, nil)
6263
}
6364

6465
// Ping attempts to verify that the server is accessible.
@@ -100,7 +101,7 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
100101

101102
ctx = driverctx.NewContextWithConnId(ctx, c.id)
102103
if len(args) > 0 {
103-
return nil, errors.New(dbsqlerr.ErrParametersNotSupported)
104+
return nil, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrParametersNotSupported, nil)
104105
}
105106
exStmtResp, opStatusResp, err := c.runQuery(ctx, query, args)
106107

@@ -122,7 +123,7 @@ func (c *conn) ExecContext(ctx context.Context, query string, args []driver.Name
122123
}
123124
if err != nil {
124125
log.Err(err).Msgf("databricks: failed to execute query: query %s", query)
125-
return nil, dbsqlerr.WrapErrf(err, "failed to execute query")
126+
return nil, dbsqlerrint.NewExecutionError(ctx, dbsqlerr.ErrQueryExecution, err, opStatusResp)
126127
}
127128

128129
res := result{AffectedRows: opStatusResp.GetNumModifiedRows()}
@@ -142,20 +143,21 @@ func (c *conn) QueryContext(ctx context.Context, query string, args []driver.Nam
142143

143144
ctx = driverctx.NewContextWithConnId(ctx, c.id)
144145
if len(args) > 0 {
145-
return nil, errors.New(dbsqlerr.ErrParametersNotSupported)
146+
return nil, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrParametersNotSupported, nil)
146147
}
147148
// first we try to get the results synchronously.
148149
// at any point in time that the context is done we must cancel and return
149-
exStmtResp, _, err := c.runQuery(ctx, query, args)
150+
exStmtResp, opStatusResp, err := c.runQuery(ctx, query, args)
150151

151152
if exStmtResp != nil && exStmtResp.OperationHandle != nil {
153+
ctx = driverctx.NewContextWithQueryId(ctx, client.SprintGuid(exStmtResp.OperationHandle.OperationId.GUID))
152154
log = logger.WithContext(c.id, driverctx.CorrelationIdFromContext(ctx), client.SprintGuid(exStmtResp.OperationHandle.OperationId.GUID))
153155
}
154156
defer log.Duration(msg, start)
155157

156158
if err != nil {
157159
log.Err(err).Msg("databricks: failed to run query") // To log query we need to redact credentials
158-
return nil, dbsqlerr.WrapErrf(err, "failed to run query")
160+
return nil, dbsqlerrint.NewExecutionError(ctx, dbsqlerr.ErrQueryExecution, err, opStatusResp)
159161
}
160162
// hold on to the operation handle
161163
opHandle := exStmtResp.OperationHandle
@@ -177,9 +179,10 @@ func (c *conn) runQuery(ctx context.Context, query string, args []driver.NamedVa
177179
}
178180
opHandle := exStmtResp.OperationHandle
179181
if opHandle != nil && opHandle.OperationId != nil {
182+
ctx = driverctx.NewContextWithQueryId(ctx, client.SprintGuid(opHandle.OperationId.GUID))
180183
log = logger.WithContext(
181184
c.id,
182-
driverctx.CorrelationIdFromContext(ctx), client.SprintGuid(opHandle.OperationId.GUID),
185+
driverctx.CorrelationIdFromContext(ctx), driverctx.QueryIdFromContext(ctx),
183186
)
184187
}
185188

@@ -217,16 +220,16 @@ func (c *conn) runQuery(ctx context.Context, query string, args []driver.NamedVa
217220
cli_service.TOperationState_ERROR_STATE,
218221
cli_service.TOperationState_TIMEDOUT_STATE:
219222
logBadQueryState(log, statusResp)
220-
return exStmtResp, statusResp, errors.New(statusResp.GetDisplayMessage())
223+
return exStmtResp, statusResp, dbsqlerrint.NewRequestError(ctx, dbsqlerr.ErrInvalidOperationState, nil)
221224
// live states
222225
default:
223226
logBadQueryState(log, statusResp)
224-
return exStmtResp, statusResp, errors.New("invalid operation state. This should not have happened")
227+
return exStmtResp, statusResp, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrInvalidOperationState, nil)
225228
}
226229
// weird states
227230
default:
228231
logBadQueryState(log, opStatus)
229-
return exStmtResp, opStatus, errors.New("invalid operation state. This should not have happened")
232+
return exStmtResp, opStatus, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrInvalidOperationState, nil)
230233
}
231234

232235
} else {
@@ -245,11 +248,11 @@ func (c *conn) runQuery(ctx context.Context, query string, args []driver.NamedVa
245248
cli_service.TOperationState_ERROR_STATE,
246249
cli_service.TOperationState_TIMEDOUT_STATE:
247250
logBadQueryState(log, statusResp)
248-
return exStmtResp, statusResp, errors.New(statusResp.GetDisplayMessage())
251+
return exStmtResp, statusResp, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrInvalidOperationState, nil)
249252
// live states
250253
default:
251254
logBadQueryState(log, statusResp)
252-
return exStmtResp, statusResp, errors.New("invalid operation state. This should not have happened")
255+
return exStmtResp, statusResp, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrInvalidOperationState, nil)
253256
}
254257
}
255258
}
@@ -311,7 +314,6 @@ func (c *conn) executeStatement(ctx context.Context, query string, args []driver
311314
} else {
312315
log.Debug().Msgf("databricks: cancel success")
313316
}
314-
315317
} else {
316318
log.Debug().Msg("databricks: query did not need cancellation")
317319
}
@@ -337,6 +339,7 @@ func (c *conn) pollOperation(ctx context.Context, opHandle *cli_service.TOperati
337339
statusResp, err = c.client.GetOperationStatus(newCtx, &cli_service.TGetOperationStatusReq{
338340
OperationHandle: opHandle,
339341
})
342+
340343
if statusResp != nil && statusResp.OperationState != nil {
341344
log.Debug().Msgf("databricks: status %s", statusResp.GetOperationState().String())
342345
}
@@ -363,13 +366,17 @@ func (c *conn) pollOperation(ctx context.Context, opHandle *cli_service.TOperati
363366
return ret, err
364367
},
365368
}
366-
_, resp, err := pollSentinel.Watch(ctx, c.cfg.PollInterval, 0)
369+
status, resp, err := pollSentinel.Watch(ctx, c.cfg.PollInterval, 0)
367370
if err != nil {
368-
return nil, dbsqlerr.WrapErr(err, "failed to poll query state")
371+
if status == sentinel.WatchTimeout {
372+
err = dbsqlerrint.NewRequestError(ctx, dbsqlerr.ErrSentinelTimeout, err)
373+
}
374+
return nil, err
369375
}
376+
370377
statusResp, ok := resp.(*cli_service.TGetOperationStatusResp)
371378
if !ok {
372-
return nil, errors.New("could not read query status")
379+
return nil, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrReadQueryStatus, nil)
373380
}
374381
return statusResp, nil
375382
}

connection_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func TestConn_executeStatement(t *testing.T) {
161161
if opTest.err == "" {
162162
assert.NoError(t, err)
163163
} else {
164-
assert.EqualError(t, err, opTest.err)
164+
assert.EqualError(t, err, "databricks: execution error: failed to execute query: "+opTest.err)
165165
}
166166
assert.Equal(t, 1, executeStatementCount)
167167
assert.Equal(t, opTest.closeOperationCount, closeOperationCount)
@@ -539,6 +539,7 @@ func TestConn_pollOperation(t *testing.T) {
539539
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
540540
defer cancel()
541541
res, err := testConn.pollOperation(ctx, &cli_service.TOperationHandle{
542+
542543
OperationId: &cli_service.THandleIdentifier{
543544
GUID: []byte{1, 2, 3, 4, 2, 23, 4, 2, 3, 1, 2, 4, 4, 223, 34, 54},
544545
Secret: []byte("b"),

connector.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import (
1010

1111
"github.com/databricks/databricks-sql-go/auth/pat"
1212
"github.com/databricks/databricks-sql-go/driverctx"
13+
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
1314
"github.com/databricks/databricks-sql-go/internal/cli_service"
1415
"github.com/databricks/databricks-sql-go/internal/client"
1516
"github.com/databricks/databricks-sql-go/internal/config"
16-
dbsqlerr "github.com/databricks/databricks-sql-go/internal/err"
17+
dbsqlerrint "github.com/databricks/databricks-sql-go/internal/errors"
1718
"github.com/databricks/databricks-sql-go/logger"
1819
)
1920

@@ -35,7 +36,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
3536

3637
tclient, err := client.InitThriftClient(c.cfg, c.client)
3738
if err != nil {
38-
return nil, dbsqlerr.WrapErr(err, "error initializing thrift client")
39+
return nil, dbsqlerrint.NewDriverError(ctx, dbsqlerr.ErrThriftClient, err)
3940
}
4041
protocolVersion := int64(c.cfg.ThriftProtocolVersion)
4142
session, err := tclient.OpenSession(ctx, &cli_service.TOpenSessionReq{
@@ -49,7 +50,7 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
4950
})
5051

5152
if err != nil {
52-
return nil, dbsqlerr.WrapErrf(err, "error connecting: host=%s port=%d, httpPath=%s", c.cfg.Host, c.cfg.Port, c.cfg.HTTPPath)
53+
return nil, dbsqlerrint.NewRequestError(ctx, fmt.Sprintf("error connecting: host=%s port=%d, httpPath=%s", c.cfg.Host, c.cfg.Port, c.cfg.HTTPPath), err)
5354
}
5455

5556
conn := &conn{

doc.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,52 @@ The result log may look like this:
144144
145145
{"level":"debug","connId":"01ed6545-5669-1ec7-8c7e-6d8a1ea0ab16","corrId":"workflow-example","queryId":"01ed6545-57cc-188a-bfc5-d9c0eaf8e189","time":1668558402,"message":"Run Main elapsed time: 1.298712292s"}
146146
147+
# Errors
148+
149+
There are three error types exposed via dbsql/errors
150+
151+
DBDriverError - An error in the go driver. Example: unimplemented functionality, invalid driver state, errors processing a server response, etc.
152+
153+
DBRequestError - An error that is caused by an invalid request. Example: permission denied, invalid http path or other connection parameter, resource not available, etc.
154+
155+
DBExecutionError - Any error that occurs after the SQL statement has been accepted such as a SQL syntax error, missing table, etc.
156+
157+
Each type has a corresponding sentinel value which can be used with errors.Is() to determine if one of the types is present in an error chain.
158+
159+
DriverError
160+
RequestError
161+
ExecutionError
162+
163+
Example usage:
164+
165+
import (
166+
fmt
167+
errors
168+
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
169+
)
170+
171+
func main() {
172+
...
173+
_, err := db.ExecContext(ogCtx, `Select id from range(100)`)
174+
if err != nil {
175+
if errors.Is(err, dbsqlerr.ExecutionError) {
176+
var execErr dbsqlerr.DBExecutionError
177+
if ok := errors.As(err, &execError); ok {
178+
fmt.Printf("%s, corrId: %s, connId: %s, queryId: %s, sqlState: %s",
179+
execErr.Error(),
180+
execErr.CorrelationId(),
181+
execErr.ConnectionId(),
182+
execErr.QueryId(),
183+
execErr.SqlState())
184+
}
185+
}
186+
...
187+
}
188+
...
189+
}
190+
191+
See the documentation for dbsql/errors for more information.
192+
147193
# Supported Data Types
148194
149195
==================================

driver_e2e_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import (
1313
"time"
1414

1515
"github.com/databricks/databricks-sql-go/driverctx"
16+
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
1617
"github.com/databricks/databricks-sql-go/internal/cli_service"
1718
"github.com/databricks/databricks-sql-go/internal/client"
18-
dbsqlerr "github.com/databricks/databricks-sql-go/internal/err"
1919
"github.com/databricks/databricks-sql-go/logger"
20+
"github.com/pkg/errors"
2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"
2223
)
@@ -281,11 +282,19 @@ func TestContextTimeoutExample(t *testing.T) {
281282
ctx1, cancel := context.WithTimeout(ogCtx, 5*time.Second)
282283
defer cancel()
283284
rows, err := db.QueryContext(ctx1, `SELECT id FROM RANGE(100000000) ORDER BY RANDOM() + 2 asc`)
284-
require.ErrorContains(t, err, context.DeadlineExceeded.Error())
285+
if err, ok := err.(interface{ StackTrace() errors.StackTrace }); ok {
286+
fmt.Printf("Stack trace: %v", err.StackTrace())
287+
}
288+
require.True(t, errors.Is(err, context.DeadlineExceeded))
289+
require.True(t, errors.Is(err, dbsqlerr.ExecutionError))
290+
var ee dbsqlerr.DBExecutionError
291+
require.True(t, errors.As(err, &ee))
292+
require.Equal(t, "context-timeout-example", ee.CorrelationId())
285293
require.Nil(t, rows)
286-
_, ok := err.(dbsqlerr.Causer)
294+
295+
_, ok := err.(interface{ Cause() error })
287296
assert.True(t, ok)
288-
_, ok = err.(dbsqlerr.StackTracer)
297+
_, ok = err.(interface{ StackTrace() errors.StackTrace })
289298
assert.True(t, ok)
290299
assert.Equal(t, 1, state.executeStatementCalls)
291300
assert.GreaterOrEqual(t, state.getOperationStatusCalls, 1)

driverctx/ctx.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,24 @@ type contextKey int
1111
const (
1212
CorrelationIdContextKey contextKey = iota
1313
ConnIdContextKey
14+
QueryIdContextKey
15+
QueryIdCallbackKey
16+
ConnIdCallbackKey
1417
)
1518

19+
type IdCallbackFunc func(string)
20+
1621
// NewContextWithCorrelationId creates a new context with correlationId value. Used by Logger to populate field corrId.
1722
func NewContextWithCorrelationId(ctx context.Context, correlationId string) context.Context {
1823
return context.WithValue(ctx, CorrelationIdContextKey, correlationId)
1924
}
2025

2126
// CorrelationIdFromContext retrieves the correlationId stored in context.
2227
func CorrelationIdFromContext(ctx context.Context) string {
28+
if ctx == nil {
29+
return ""
30+
}
31+
2332
corrId, ok := ctx.Value(CorrelationIdContextKey).(string)
2433
if !ok {
2534
return ""
@@ -29,14 +38,51 @@ func CorrelationIdFromContext(ctx context.Context) string {
2938

3039
// NewContextWithConnId creates a new context with connectionId value.
3140
func NewContextWithConnId(ctx context.Context, connId string) context.Context {
41+
if callback, ok := ctx.Value(ConnIdCallbackKey).(IdCallbackFunc); ok {
42+
callback(connId)
43+
}
3244
return context.WithValue(ctx, ConnIdContextKey, connId)
3345
}
3446

3547
// ConnIdFromContext retrieves the connectionId stored in context.
3648
func ConnIdFromContext(ctx context.Context) string {
49+
if ctx == nil {
50+
return ""
51+
}
52+
3753
connId, ok := ctx.Value(ConnIdContextKey).(string)
3854
if !ok {
3955
return ""
4056
}
4157
return connId
4258
}
59+
60+
// NewContextWithQueryId creates a new context with queryId value.
61+
func NewContextWithQueryId(ctx context.Context, queryId string) context.Context {
62+
if callback, ok := ctx.Value(QueryIdCallbackKey).(IdCallbackFunc); ok {
63+
callback(queryId)
64+
}
65+
66+
return context.WithValue(ctx, QueryIdContextKey, queryId)
67+
}
68+
69+
// QueryIdFromContext retrieves the queryId stored in context.
70+
func QueryIdFromContext(ctx context.Context) string {
71+
if ctx == nil {
72+
return ""
73+
}
74+
75+
queryId, ok := ctx.Value(QueryIdContextKey).(string)
76+
if !ok {
77+
return ""
78+
}
79+
return queryId
80+
}
81+
82+
func NewContextWithQueryIdCallback(ctx context.Context, callback IdCallbackFunc) context.Context {
83+
return context.WithValue(ctx, QueryIdCallbackKey, callback)
84+
}
85+
86+
func NewContextWithConnIdCallback(ctx context.Context, callback IdCallbackFunc) context.Context {
87+
return context.WithValue(ctx, ConnIdCallbackKey, callback)
88+
}

0 commit comments

Comments
 (0)