Skip to content

Commit f0ac63c

Browse files
authored
Support ReturnValuesOnConditionCheckFailure (#246)
* support ReturnValuesOnConditionCheckFailure (#245) * add IncludeItemInCondCheckFail and friends * add IncludeItemInCondCheckFail to ConditionCheck
1 parent eed9493 commit f0ac63c

File tree

12 files changed

+342
-129
lines changed

12 files changed

+342
-129
lines changed

conditioncheck.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ type ConditionCheck struct {
1616
rangeKey string
1717
rangeValue types.AttributeValue
1818

19-
condition string
19+
condition string
20+
onCondFail types.ReturnValuesOnConditionCheckFailure
2021
subber
2122

2223
err error
@@ -74,6 +75,15 @@ func (check *ConditionCheck) IfNotExists() *ConditionCheck {
7475
return check.If("attribute_not_exists($)", check.hashKey)
7576
}
7677

78+
func (check *ConditionCheck) IncludeItemInCondCheckFail(enabled bool) *ConditionCheck {
79+
if enabled {
80+
check.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
81+
} else {
82+
check.onCondFail = types.ReturnValuesOnConditionCheckFailureNone
83+
}
84+
return check
85+
}
86+
7787
func (check *ConditionCheck) writeTxItem() (*types.TransactWriteItem, error) {
7888
if check.err != nil {
7989
return nil, check.err
@@ -86,6 +96,7 @@ func (check *ConditionCheck) writeTxItem() (*types.TransactWriteItem, error) {
8696
}
8797
if check.condition != "" {
8898
item.ConditionExpression = aws.String(check.condition)
99+
item.ReturnValuesOnConditionCheckFailure = check.onCondFail
89100
}
90101
return &types.TransactWriteItem{
91102
ConditionCheck: item,

db.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,45 @@ func IsCondCheckFailed(err error) bool {
203203
return false
204204
}
205205

206-
// type noopLogger struct{}
206+
// Unmarshals an item from a ConditionalCheckFailedException into `out`, with the same behavior as [UnmarshalItem].
207+
// The return value boolean `match` will be true if condCheckErr is a ConditionalCheckFailedException,
208+
// otherwise false if it is nil or a different error.
209+
func UnmarshalItemFromCondCheckFailed(condCheckErr error, out any) (match bool, err error) {
210+
if condCheckErr == nil {
211+
return false, nil
212+
}
213+
var cfe *types.ConditionalCheckFailedException
214+
if errors.As(condCheckErr, &cfe) {
215+
if cfe.Item == nil {
216+
return true, fmt.Errorf("dynamo: ConditionalCheckFailedException does not contain item (is IncludeItemInCondCheckFail disabled?): %w", condCheckErr)
217+
}
218+
return true, UnmarshalItem(cfe.Item, out)
219+
}
220+
return false, condCheckErr
221+
}
207222

208-
// func (noopLogger) Log(...interface{}) {}
223+
// Unmarshals items from a TransactionCanceledException by appending them to `out`, which must be a pointer to a slice.
224+
// The return value boolean `match` will be true if txCancelErr is a TransactionCanceledException with at least one ConditionalCheckFailed cancellation reason,
225+
// otherwise false if it is nil or a different error.
226+
func UnmarshalItemsFromTxCondCheckFailed(txCancelErr error, out any) (match bool, err error) {
227+
if txCancelErr == nil {
228+
return false, nil
229+
}
230+
unmarshal := unmarshalAppendTo(out)
231+
var txe *types.TransactionCanceledException
232+
if errors.As(txCancelErr, &txe) {
233+
for _, cr := range txe.CancellationReasons {
234+
if cr.Code != nil && *cr.Code == "ConditionalCheckFailed" {
235+
if cr.Item == nil {
236+
return true, fmt.Errorf("dynamo: TransactionCanceledException.CancellationReasons does not contain item (is IncludeItemInCondCheckFail disabled?): %w", txCancelErr)
237+
}
238+
if err = unmarshal(cr.Item, out); err != nil {
239+
return true, err
240+
}
241+
match = true
242+
}
243+
}
244+
return match, nil
245+
}
246+
return false, txCancelErr
247+
}

delete.go

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
// Delete is a request to delete an item.
1212
// See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html
1313
type Delete struct {
14-
table Table
15-
returnType string
14+
table Table
15+
16+
returnType types.ReturnValue
17+
onCondFail types.ReturnValuesOnConditionCheckFailure
1618

1719
hashKey string
1820
hashValue types.AttributeValue
@@ -79,15 +81,15 @@ func (d *Delete) ConsumedCapacity(cc *ConsumedCapacity) *Delete {
7981

8082
// Run executes this delete request.
8183
func (d *Delete) Run(ctx context.Context) error {
82-
d.returnType = "NONE"
84+
d.returnType = types.ReturnValueNone
8385
_, err := d.run(ctx)
8486
return err
8587
}
8688

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

103+
// CurrentValue executes this delete.
104+
// If successful, the return value `deleted` will be true, and nothing will be unmarshaled to `out`
105+
//
106+
// 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.
107+
//
108+
// If the delete is unsuccessful for any other reason, `deleted` will be false and `err` will be non-nil.
109+
//
110+
// See also: [UnmarshalItemFromCondCheckFailed].
111+
func (d *Delete) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) {
112+
d.returnType = types.ReturnValueNone
113+
d.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
114+
_, err = d.run(ctx)
115+
if err != nil {
116+
if ok, err := UnmarshalItemFromCondCheckFailed(err, out); ok {
117+
return false, err
118+
}
119+
return false, err
120+
}
121+
return true, nil
122+
}
123+
124+
// IncludeAllItemsInCondCheckFail specifies whether an item delete that fails its condition check should include the item itself in the error.
125+
// Such items can be extracted using [UnmarshalItemFromCondCheckFailed] for single deletes, or [UnmarshalItemsFromTxCondCheckFailed] for write transactions.
126+
func (d *Delete) IncludeItemInCondCheckFail(enabled bool) *Delete {
127+
if enabled {
128+
d.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
129+
} else {
130+
d.onCondFail = types.ReturnValuesOnConditionCheckFailureNone
131+
}
132+
return d
133+
}
134+
101135
func (d *Delete) run(ctx context.Context) (*dynamodb.DeleteItemOutput, error) {
102136
if d.err != nil {
103137
return nil, d.err
@@ -121,12 +155,13 @@ func (d *Delete) deleteInput() *dynamodb.DeleteItemInput {
121155
input := &dynamodb.DeleteItemInput{
122156
TableName: &d.table.name,
123157
Key: d.key(),
124-
ReturnValues: types.ReturnValue(d.returnType),
158+
ReturnValues: d.returnType,
125159
ExpressionAttributeNames: d.nameExpr,
126160
ExpressionAttributeValues: d.valueExpr,
127161
}
128162
if d.condition != "" {
129163
input.ConditionExpression = &d.condition
164+
input.ReturnValuesOnConditionCheckFailure = d.onCondFail
130165
}
131166
if d.cc != nil {
132167
input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes
@@ -141,11 +176,12 @@ func (d *Delete) writeTxItem() (*types.TransactWriteItem, error) {
141176
input := d.deleteInput()
142177
item := &types.TransactWriteItem{
143178
Delete: &types.Delete{
144-
TableName: input.TableName,
145-
Key: input.Key,
146-
ExpressionAttributeNames: input.ExpressionAttributeNames,
147-
ExpressionAttributeValues: input.ExpressionAttributeValues,
148-
ConditionExpression: input.ConditionExpression,
179+
TableName: input.TableName,
180+
Key: input.Key,
181+
ExpressionAttributeNames: input.ExpressionAttributeNames,
182+
ExpressionAttributeValues: input.ExpressionAttributeValues,
183+
ConditionExpression: input.ConditionExpression,
184+
ReturnValuesOnConditionCheckFailure: input.ReturnValuesOnConditionCheckFailure,
149185
},
150186
}
151187
return item, nil

delete_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ func TestDelete(t *testing.T) {
2929
}
3030

3131
// fail to delete it
32-
err = table.Delete("UserID", item.UserID).
32+
var curr widget
33+
wrote, err := table.Delete("UserID", item.UserID).
3334
Range("Time", item.Time).
3435
If("Meta.'color' = ?", "octarine").
3536
If("Msg = ?", "wrong msg").
36-
Run(ctx)
37-
if !IsCondCheckFailed(err) {
38-
t.Error("expected ConditionalCheckFailedException, not", err)
37+
CurrentValue(ctx, &curr)
38+
if wrote {
39+
t.Error("wrote should be false")
40+
}
41+
if !reflect.DeepEqual(curr, item) {
42+
t.Errorf("bad value. %#v ≠ %#v", curr, item)
3943
}
4044

4145
// delete it

go.mod

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
module github.com/guregu/dynamo/v2
22

33
require (
4-
github.com/aws/aws-sdk-go-v2 v1.30.3
4+
github.com/aws/aws-sdk-go-v2 v1.30.4
55
github.com/aws/aws-sdk-go-v2/config v1.11.0
66
github.com/aws/aws-sdk-go-v2/credentials v1.6.4
7-
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.9
8-
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.3
9-
github.com/aws/smithy-go v1.20.3
7+
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.11
8+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5
9+
github.com/aws/smithy-go v1.20.4
1010
github.com/cenkalti/backoff/v4 v4.3.0
11-
golang.org/x/sync v0.7.0
11+
golang.org/x/sync v0.8.0
1212
)
1313

1414
require (
1515
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
16-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
17-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
16+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
17+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
1818
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
19-
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 // indirect
20-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
21-
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 // indirect
19+
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.4 // indirect
20+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
21+
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17 // indirect
2222
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
2323
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
2424
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect

0 commit comments

Comments
 (0)