Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ReturnValuesOnConditionCheckFailure (#245) #246

Merged
merged 3 commits into from
Aug 26, 2024
Merged
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
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
Loading