Skip to content

Commit

Permalink
add IncludeItemInCondCheckFail and friends
Browse files Browse the repository at this point in the history
  • Loading branch information
guregu committed Aug 23, 2024
1 parent 1519c9c commit 973bed6
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 14 deletions.
4 changes: 2 additions & 2 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func UnmarshalItemFromCondCheckFailed(condCheckErr error, out any) (match bool,
var cfe *types.ConditionalCheckFailedException
if errors.As(condCheckErr, &cfe) {
if cfe.Item == nil {
return true, fmt.Errorf("dynamo: ConditionalCheckFailedException does not contain item")
return true, fmt.Errorf("dynamo: ConditionalCheckFailedException does not contain item (is IncludeItemInCondCheckFail disabled?): %w", condCheckErr)
}
return true, UnmarshalItem(cfe.Item, out)
}
Expand All @@ -233,7 +233,7 @@ func UnmarshalItemsFromTxCondCheckFailed(txCancelErr error, out any) (match bool
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")
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
Expand Down
18 changes: 16 additions & 2 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
table Table

returnType types.ReturnValue
onCondFail types.ReturnValuesOnConditionCheckFailure

hashKey string
hashValue types.AttributeValue
Expand Down Expand Up @@ -108,6 +110,7 @@ func (d *Delete) OldValue(ctx context.Context, out interface{}) error {
// 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 {
Expand All @@ -118,6 +121,17 @@ func (d *Delete) CurrentValue(ctx context.Context, out interface{}) (wrote bool,
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 Down Expand Up @@ -147,7 +161,7 @@ func (d *Delete) deleteInput() *dynamodb.DeleteItemInput {
}
if d.condition != "" {
input.ConditionExpression = &d.condition
input.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld
input.ReturnValuesOnConditionCheckFailure = d.onCondFail
}
if d.cc != nil {
input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes
Expand Down
18 changes: 16 additions & 2 deletions put.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
// Put is a request to create or replace an item.
// See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html
type Put struct {
table Table
table Table

returnType types.ReturnValue
onCondFail types.ReturnValuesOnConditionCheckFailure

item Item
subber
Expand Down Expand Up @@ -83,6 +85,7 @@ func (p *Put) OldValue(ctx context.Context, out interface{}) error {
// See also: [UnmarshalItemFromCondCheckFailed].
func (p *Put) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) {
p.returnType = types.ReturnValueNone
p.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
item, _, err := p.run(ctx)
wrote = err == nil
if err != nil {
Expand All @@ -93,6 +96,17 @@ func (p *Put) CurrentValue(ctx context.Context, out interface{}) (wrote bool, er
return
}

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

func (p *Put) run(ctx context.Context) (item Item, output *dynamodb.PutItemOutput, err error) {
if p.err != nil {
return nil, nil, p.err
Expand Down Expand Up @@ -121,7 +135,7 @@ func (p *Put) input() *dynamodb.PutItemInput {
}
if p.condition != "" {
input.ConditionExpression = &p.condition
input.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld
input.ReturnValuesOnConditionCheckFailure = p.onCondFail
}
if p.cc != nil {
input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes
Expand Down
5 changes: 4 additions & 1 deletion put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ func TestPut(t *testing.T) {

// putting the same item: this should fail
t.Run("UnmarshalItemFromCondCheckFailed", func(t *testing.T) {
err := table.Put(newItem).If("attribute_not_exists(UserID)").If("attribute_not_exists('Time')").Run(ctx)
err := table.Put(newItem).
If("attribute_not_exists(UserID)").
If("attribute_not_exists('Time')").
IncludeItemInCondCheckFail(true).Run(ctx)
if !IsCondCheckFailed(err) {
t.Error("expected ConditionalCheckFailedException, not", err)
}
Expand Down
42 changes: 37 additions & 5 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ type writeTxOp interface {
// WriteTx is analogous to TransactWriteItems in DynamoDB's API.
// See: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html
type WriteTx struct {
db *DB
items []writeTxOp
token string
cc *ConsumedCapacity
err error
db *DB
items []writeTxOp
token string
onCondFail types.ReturnValuesOnConditionCheckFailure
cc *ConsumedCapacity
err error
}

// WriteTx begins a new write transaction.
Expand Down Expand Up @@ -206,6 +207,20 @@ func (tx *WriteTx) Check(check *ConditionCheck) *WriteTx {
return tx
}

// IncludeAllItemsInCondCheckFail specifies whether an item write that fails its condition check should include the item itself in the error.
// Such items can be extracted using [UnmarshalItemsFromTxCondCheckFailed].
//
// By default, the individual settings for each item are respected.
// Calling this will override all individual settings.
func (tx *WriteTx) IncludeAllItemsInCondCheckFail(enabled bool) *WriteTx {
if enabled {
tx.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
} else {
tx.onCondFail = types.ReturnValuesOnConditionCheckFailureNone
}
return tx
}

// Idempotent marks this transaction as idempotent when enabled is true.
// This automatically generates a unique idempotency token for you.
// An idempotent transaction ran multiple times will have the same effect as being run once.
Expand Down Expand Up @@ -279,6 +294,7 @@ func (tx *WriteTx) input() (*dynamodb.TransactWriteItemsInput, error) {
if err != nil {
return nil, err
}
setTWIReturnType(wti, tx.onCondFail)
input.TransactItems = append(input.TransactItems, *wti)
}
if tx.token != "" {
Expand All @@ -290,6 +306,22 @@ func (tx *WriteTx) input() (*dynamodb.TransactWriteItemsInput, error) {
return input, nil
}

func setTWIReturnType(wti *types.TransactWriteItem, ret types.ReturnValuesOnConditionCheckFailure) {
if ret == "" {
return
}
switch {
case wti.ConditionCheck != nil:
wti.ConditionCheck.ReturnValuesOnConditionCheckFailure = ret
case wti.Delete != nil:
wti.Delete.ReturnValuesOnConditionCheckFailure = ret
case wti.Put != nil:
wti.Put.ReturnValuesOnConditionCheckFailure = ret
case wti.Update != nil:
wti.Update.ReturnValuesOnConditionCheckFailure = ret
}
}

func (tx *WriteTx) setError(err error) {
if tx.err == nil {
tx.err = err
Expand Down
1 change: 1 addition & 0 deletions tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func TestTx(t *testing.T) {
tx := testDB.WriteTx()
tx.Put(table.Put(widget{UserID: 69, Time: date1}).If("'BadField' = ?", "should not exist"))
tx.Put(table.Put(widget{UserID: 69, Time: date2}).If("'BadField' = ?", "should not exist"))
tx.IncludeAllItemsInCondCheckFail(true)
err := tx.Run(ctx)
if err == nil {
t.Fatal("expected error")
Expand Down
18 changes: 16 additions & 2 deletions update.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
// It uses the UpdateItem API.
// See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html
type Update struct {
table Table
table Table

returnType types.ReturnValue
onCondFail types.ReturnValuesOnConditionCheckFailure

hashKey string
hashValue types.AttributeValue
Expand Down Expand Up @@ -347,6 +349,7 @@ func (u *Update) OnlyUpdatedOldValue(ctx context.Context, out interface{}) error
// See also: [UnmarshalItemFromCondCheckFailed].
func (u *Update) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) {
u.returnType = types.ReturnValueAllNew
u.onCondFail = types.ReturnValuesOnConditionCheckFailureAllOld
output, err := u.run(ctx)
if err != nil {
if ok, err := UnmarshalItemFromCondCheckFailed(err, out); ok {
Expand All @@ -357,6 +360,17 @@ func (u *Update) CurrentValue(ctx context.Context, out interface{}) (wrote bool,
return true, unmarshalItem(output.Attributes, out)
}

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

func (u *Update) run(ctx context.Context) (*dynamodb.UpdateItemOutput, error) {
if u.err != nil {
return nil, u.err
Expand Down Expand Up @@ -387,7 +401,7 @@ func (u *Update) updateInput() *dynamodb.UpdateItemInput {
}
if u.condition != "" {
input.ConditionExpression = &u.condition
input.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld
input.ReturnValuesOnConditionCheckFailure = u.onCondFail
}
if u.cc != nil {
input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes
Expand Down

0 comments on commit 973bed6

Please sign in to comment.