Skip to content

Commit

Permalink
Support ReturnValuesOnConditionCheckFailure (#246)
Browse files Browse the repository at this point in the history
* support ReturnValuesOnConditionCheckFailure (#245)

* add IncludeItemInCondCheckFail and friends

* add IncludeItemInCondCheckFail to ConditionCheck
  • Loading branch information
guregu authored Aug 26, 2024
1 parent eed9493 commit f0ac63c
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 129 deletions.
13 changes: 12 additions & 1 deletion conditioncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ type ConditionCheck struct {
rangeKey string
rangeValue types.AttributeValue

condition string
condition string
onCondFail types.ReturnValuesOnConditionCheckFailure
subber

err error
Expand Down Expand Up @@ -74,6 +75,15 @@ func (check *ConditionCheck) IfNotExists() *ConditionCheck {
return check.If("attribute_not_exists($)", check.hashKey)
}

func (check *ConditionCheck) IncludeItemInCondCheckFail(enabled bool) *ConditionCheck {
if enabled {
check.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
} else {
check.onCondFail = types.ReturnValuesOnConditionCheckFailureNone
}
return check
}

func (check *ConditionCheck) writeTxItem() (*types.TransactWriteItem, error) {
if check.err != nil {
return nil, check.err
Expand All @@ -86,6 +96,7 @@ func (check *ConditionCheck) writeTxItem() (*types.TransactWriteItem, error) {
}
if check.condition != "" {
item.ConditionExpression = aws.String(check.condition)
item.ReturnValuesOnConditionCheckFailure = check.onCondFail
}
return &types.TransactWriteItem{
ConditionCheck: item,
Expand Down
43 changes: 41 additions & 2 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,45 @@ func IsCondCheckFailed(err error) bool {
return false
}

// type noopLogger struct{}
// Unmarshals an item from a ConditionalCheckFailedException into `out`, with the same behavior as [UnmarshalItem].
// The return value boolean `match` will be true if condCheckErr is a ConditionalCheckFailedException,
// otherwise false if it is nil or a different error.
func UnmarshalItemFromCondCheckFailed(condCheckErr error, out any) (match bool, err error) {
if condCheckErr == nil {
return false, nil
}
var cfe *types.ConditionalCheckFailedException
if errors.As(condCheckErr, &cfe) {
if cfe.Item == nil {
return true, fmt.Errorf("dynamo: ConditionalCheckFailedException does not contain item (is IncludeItemInCondCheckFail disabled?): %w", condCheckErr)
}
return true, UnmarshalItem(cfe.Item, out)
}
return false, condCheckErr
}

// func (noopLogger) Log(...interface{}) {}
// Unmarshals items from a TransactionCanceledException by appending them to `out`, which must be a pointer to a slice.
// The return value boolean `match` will be true if txCancelErr is a TransactionCanceledException with at least one ConditionalCheckFailed cancellation reason,
// otherwise false if it is nil or a different error.
func UnmarshalItemsFromTxCondCheckFailed(txCancelErr error, out any) (match bool, err error) {
if txCancelErr == nil {
return false, nil
}
unmarshal := unmarshalAppendTo(out)
var txe *types.TransactionCanceledException
if errors.As(txCancelErr, &txe) {
for _, cr := range txe.CancellationReasons {
if cr.Code != nil && *cr.Code == "ConditionalCheckFailed" {
if cr.Item == nil {
return true, fmt.Errorf("dynamo: TransactionCanceledException.CancellationReasons does not contain item (is IncludeItemInCondCheckFail disabled?): %w", txCancelErr)
}
if err = unmarshal(cr.Item, out); err != nil {
return true, err
}
match = true
}
}
return match, nil
}
return false, txCancelErr
}
56 changes: 46 additions & 10 deletions delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
// Delete is a request to delete an item.
// See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html
type Delete struct {
table Table
returnType string
table Table

returnType types.ReturnValue
onCondFail types.ReturnValuesOnConditionCheckFailure

hashKey string
hashValue types.AttributeValue
Expand Down Expand Up @@ -79,15 +81,15 @@ func (d *Delete) ConsumedCapacity(cc *ConsumedCapacity) *Delete {

// Run executes this delete request.
func (d *Delete) Run(ctx context.Context) error {
d.returnType = "NONE"
d.returnType = types.ReturnValueNone
_, err := d.run(ctx)
return err
}

// OldValue executes this delete request, unmarshaling the previous value to out.
// Returns ErrNotFound is there was no previous value.
func (d *Delete) OldValue(ctx context.Context, out interface{}) error {
d.returnType = "ALL_OLD"
d.returnType = types.ReturnValueAllOld
output, err := d.run(ctx)
switch {
case err != nil:
Expand All @@ -98,6 +100,38 @@ func (d *Delete) OldValue(ctx context.Context, out interface{}) error {
return unmarshalItem(output.Attributes, out)
}

// CurrentValue executes this delete.
// If successful, the return value `deleted` will be true, and nothing will be unmarshaled to `out`
//
// If the delete is unsuccessful because of a condition check failure, `deleted` will be false, the current value of the item will be unmarshaled to `out`, and `err` will be nil.
//
// If the delete is unsuccessful for any other reason, `deleted` will be false and `err` will be non-nil.
//
// See also: [UnmarshalItemFromCondCheckFailed].
func (d *Delete) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) {
d.returnType = types.ReturnValueNone
d.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
_, err = d.run(ctx)
if err != nil {
if ok, err := UnmarshalItemFromCondCheckFailed(err, out); ok {
return false, err
}
return false, err
}
return true, nil
}

// IncludeAllItemsInCondCheckFail specifies whether an item delete that fails its condition check should include the item itself in the error.
// Such items can be extracted using [UnmarshalItemFromCondCheckFailed] for single deletes, or [UnmarshalItemsFromTxCondCheckFailed] for write transactions.
func (d *Delete) IncludeItemInCondCheckFail(enabled bool) *Delete {
if enabled {
d.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
} else {
d.onCondFail = types.ReturnValuesOnConditionCheckFailureNone
}
return d
}

func (d *Delete) run(ctx context.Context) (*dynamodb.DeleteItemOutput, error) {
if d.err != nil {
return nil, d.err
Expand All @@ -121,12 +155,13 @@ func (d *Delete) deleteInput() *dynamodb.DeleteItemInput {
input := &dynamodb.DeleteItemInput{
TableName: &d.table.name,
Key: d.key(),
ReturnValues: types.ReturnValue(d.returnType),
ReturnValues: d.returnType,
ExpressionAttributeNames: d.nameExpr,
ExpressionAttributeValues: d.valueExpr,
}
if d.condition != "" {
input.ConditionExpression = &d.condition
input.ReturnValuesOnConditionCheckFailure = d.onCondFail
}
if d.cc != nil {
input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes
Expand All @@ -141,11 +176,12 @@ func (d *Delete) writeTxItem() (*types.TransactWriteItem, error) {
input := d.deleteInput()
item := &types.TransactWriteItem{
Delete: &types.Delete{
TableName: input.TableName,
Key: input.Key,
ExpressionAttributeNames: input.ExpressionAttributeNames,
ExpressionAttributeValues: input.ExpressionAttributeValues,
ConditionExpression: input.ConditionExpression,
TableName: input.TableName,
Key: input.Key,
ExpressionAttributeNames: input.ExpressionAttributeNames,
ExpressionAttributeValues: input.ExpressionAttributeValues,
ConditionExpression: input.ConditionExpression,
ReturnValuesOnConditionCheckFailure: input.ReturnValuesOnConditionCheckFailure,
},
}
return item, nil
Expand Down
12 changes: 8 additions & 4 deletions delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ func TestDelete(t *testing.T) {
}

// fail to delete it
err = table.Delete("UserID", item.UserID).
var curr widget
wrote, err := table.Delete("UserID", item.UserID).
Range("Time", item.Time).
If("Meta.'color' = ?", "octarine").
If("Msg = ?", "wrong msg").
Run(ctx)
if !IsCondCheckFailed(err) {
t.Error("expected ConditionalCheckFailedException, not", err)
CurrentValue(ctx, &curr)
if wrote {
t.Error("wrote should be false")
}
if !reflect.DeepEqual(curr, item) {
t.Errorf("bad value. %#v ≠ %#v", curr, item)
}

// delete it
Expand Down
20 changes: 10 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
module github.com/guregu/dynamo/v2

require (
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/aws/aws-sdk-go-v2 v1.30.4
github.com/aws/aws-sdk-go-v2/config v1.11.0
github.com/aws/aws-sdk-go-v2/credentials v1.6.4
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.9
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.3
github.com/aws/smithy-go v1.20.3
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.11
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5
github.com/aws/smithy-go v1.20.4
github.com/cenkalti/backoff/v4 v4.3.0
golang.org/x/sync v0.7.0
golang.org/x/sync v0.8.0
)

require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
Expand Down
Loading

0 comments on commit f0ac63c

Please sign in to comment.