Skip to content

Commit

Permalink
feat(spanner): demonstrate enhanced row not found handling options go…
Browse files Browse the repository at this point in the history
  • Loading branch information
husam-e committed Jun 11, 2024
1 parent 9799e02 commit 6ac6b82
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 2 deletions.
14 changes: 13 additions & 1 deletion spanner/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/protoadapt"
)

// Error is the structured error returned by Cloud Spanner client.
Expand Down Expand Up @@ -65,6 +66,10 @@ type TransactionOutcomeUnknownError struct {
err error
}

// RowNotFoundReason represents the [errdetails.ErrorInfo.Reason] used when a
// row is not found by some key.
const RowNotFoundReason = "ROW_NOT_FOUND"

const transactionOutcomeUnknownMsg = "transaction outcome unknown"

// Error implements error.Error.
Expand Down Expand Up @@ -116,8 +121,15 @@ func (e *Error) decorate(info string) {
// spannerErrorf generates a *spanner.Error with the given description and an
// APIError error having given error code as its status.
func spannerErrorf(code codes.Code, format string, args ...interface{}) error {
return spannerErrorfWithDetails(code, nil, format, args)
}

// spannerErrorfWithDetails generates a *spanner.Error with the given description and an
// APIError error having given error code as its status, and the details attached.
func spannerErrorfWithDetails(code codes.Code, details []protoadapt.MessageV1, format string, args ...interface{}) error {
msg := fmt.Sprintf(format, args...)
wrapped, _ := apierror.FromError(status.Error(code, msg))
statusErr, _ := status.New(code, msg).WithDetails(details...)
wrapped, _ := apierror.FromError(statusErr.Err())
return &Error{
Code: code,
err: wrapped,
Expand Down
47 changes: 46 additions & 1 deletion spanner/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import (
"google.golang.org/api/iterator"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"

vkit "cloud.google.com/go/spanner/apiv1"
durationpb "google.golang.org/protobuf/types/known/durationpb"
Expand Down Expand Up @@ -326,7 +328,8 @@ func (t *txReadOnly) ReadWithOptions(ctx context.Context, table string, keys Key
// errRowNotFound returns error for not being able to read the row identified by
// key.
func errRowNotFound(table string, key Key) error {
return spannerErrorf(codes.NotFound, "row not found(Table: %v, PrimaryKey: %v)", table, key)
details := []protoadapt.MessageV1{&errdetails.ErrorInfo{Reason: RowNotFoundReason}}
return spannerErrorfWithDetails(codes.NotFound, details, "row not found(Table: %v, PrimaryKey: %v)", table, key)
}

// errRowNotFoundByIndex returns error for not being able to read the row by index.
Expand All @@ -348,10 +351,38 @@ func errInlineBeginTransactionFailed() error {
//
// If no row is present with the given key, then ReadRow returns an error where
// spanner.ErrCode(err) is codes.NotFound.
//
// To reliably determine if an error is due to the row not being found, unwrap the
// error as an APIError, and compare its ErrorInfo.Reason to RowNotFoundReason.
//
// Example:
// var apiErr *apierror.APIError
// _, err := spanner.Single().ReadRow(...)
//
// if err != nil {
// errors.As(err, &apiErr)
// if apiErr.Details.ErrorInfo.Reason == spanner.RowNotFoundReason {
// // handle row not found
// }
// }
func (t *txReadOnly) ReadRow(ctx context.Context, table string, key Key, columns []string) (*Row, error) {
return t.ReadRowWithOptions(ctx, table, key, columns, nil)
}

// ReadRowOptional reads a single row from the database.
//
// If no row is present with the given key, then ReadRowOptional returns nil.
// This is a convenience function to simplify use cases that want to treat
// a missing row as nil in a reliable manner, without matching against the
// error message.
func (t *txReadOnly) ReadRowOptional(ctx context.Context, table string, key Key, columns []string) (*Row, error) {
row, err := t.ReadRowWithOptions(ctx, table, key, columns, nil)
if err == errRowNotFound(table, key) {
return nil, nil
}
return row, err
}

// ReadRowWithOptions reads a single row from the database. Pass a ReadOptions to modify the read operation.
//
// If no row is present with the given key, then ReadRowWithOptions returns an error where
Expand All @@ -375,6 +406,20 @@ func (t *txReadOnly) ReadRowWithOptions(ctx context.Context, table string, key K
// If no row is present with the given index, then ReadRowUsingIndex returns an
// error where spanner.ErrCode(err) is codes.NotFound.
//
// To reliably determine if an error is due to the row not being found, unwrap the
// error as an APIError, and compare its ErrorInfo.Reason to RowNotFoundReason.
//
// Example:
// var apiErr *apierror.APIError
// _, err := spanner.Single().ReadRowUsingIndex(...)
//
// if err != nil {
// errors.As(err, &apiErr)
// if apiErr.Details.ErrorInfo.Reason == spanner.RowNotFoundReason {
// // handle row not found
// }
// }
//
// If more than one row received with the given index, then ReadRowUsingIndex
// returns an error where spanner.ErrCode(err) is codes.FailedPrecondition.
func (t *txReadOnly) ReadRowUsingIndex(ctx context.Context, table string, index string, key Key, columns []string) (*Row, error) {
Expand Down

0 comments on commit 6ac6b82

Please sign in to comment.