From 1519c9c4ce6bcad05a9969c28ba630e90a92ebfa Mon Sep 17 00:00:00 2001 From: guregu Date: Fri, 23 Aug 2024 01:45:05 +0900 Subject: [PATCH] support ReturnValuesOnConditionCheckFailure (#245) --- conditioncheck.go | 1 + db.go | 43 ++++++++++++++++++++++++-- delete.go | 40 ++++++++++++++++++------ delete_test.go | 12 ++++--- go.mod | 20 ++++++------ go.sum | 79 +++++++++++++---------------------------------- put.go | 51 +++++++++++++++++++++--------- put_test.go | 30 +++++++++++++++--- tx_test.go | 28 +++++++++++++++++ update.go | 49 ++++++++++++++++++++--------- update_test.go | 12 ++++--- 11 files changed, 245 insertions(+), 120 deletions(-) diff --git a/conditioncheck.go b/conditioncheck.go index 3c59fd1..0f51110 100644 --- a/conditioncheck.go +++ b/conditioncheck.go @@ -86,6 +86,7 @@ func (check *ConditionCheck) writeTxItem() (*types.TransactWriteItem, error) { } if check.condition != "" { item.ConditionExpression = aws.String(check.condition) + item.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld } return &types.TransactWriteItem{ ConditionCheck: item, diff --git a/db.go b/db.go index 511e297..4c726ed 100644 --- a/db.go +++ b/db.go @@ -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") + } + 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") + } + if err = unmarshal(cr.Item, out); err != nil { + return true, err + } + match = true + } + } + return match, nil + } + return false, txCancelErr +} diff --git a/delete.go b/delete.go index c38e621..d7be2cf 100644 --- a/delete.go +++ b/delete.go @@ -12,7 +12,7 @@ import ( // See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html type Delete struct { table Table - returnType string + returnType types.ReturnValue hashKey string hashValue types.AttributeValue @@ -79,7 +79,7 @@ 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 } @@ -87,7 +87,7 @@ func (d *Delete) Run(ctx context.Context) error { // 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: @@ -98,6 +98,26 @@ 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 + _, err = d.run(ctx) + if err != nil { + if ok, err := UnmarshalItemFromCondCheckFailed(err, out); ok { + return false, err + } + return false, err + } + return true, nil +} + func (d *Delete) run(ctx context.Context) (*dynamodb.DeleteItemOutput, error) { if d.err != nil { return nil, d.err @@ -121,12 +141,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 = types.ReturnValuesOnConditionCheckFailureAllOld } if d.cc != nil { input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes @@ -141,11 +162,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 diff --git a/delete_test.go b/delete_test.go index feae4c2..381d1a6 100644 --- a/delete_test.go +++ b/delete_test.go @@ -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 diff --git a/go.mod b/go.mod index 877e2cf..8cd219f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f8dea77..54f3708 100644 --- a/go.sum +++ b/go.sum @@ -1,79 +1,46 @@ github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ= -github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= -github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2 v1.27.2 h1:pLsTXqX93rimAOZG2FIYraDQstZaaGVVN4tNw65v0h8= -github.com/aws/aws-sdk-go-v2 v1.27.2/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= -github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o= -github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= +github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= github.com/aws/aws-sdk-go-v2/config v1.11.0 h1:Czlld5zBB61A3/aoegA9/buZulwL9mHHfizh/Oq+Kqs= github.com/aws/aws-sdk-go-v2/config v1.11.0/go.mod h1:VrQDJGFBM5yZe+IOeenNZ/DWoErdny+k2MHEIpwDsEY= github.com/aws/aws-sdk-go-v2/credentials v1.6.4 h1:2hvbUoHufns0lDIsaK8FVCMukT1WngtZPavN+W2FkSw= github.com/aws/aws-sdk-go-v2/credentials v1.6.4/go.mod h1:tTrhvBPHyPde4pdIPSba4Nv7RYr4wP9jxXEDa1bKn/8= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4 h1:9WteVf5jmManG9HlxTFsk1+MT1IZ8S/8rvR+3A3OKng= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.4.4/go.mod h1:MWyvQ5I9fEsoV+Im6IgpILXlAaypjlRqUkyS5GP5pIo= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.1 h1:Uhn/kOwwHAL4vI6LdgvV0cfaQbaLyvJbCCyrSZLNBm8= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.1/go.mod h1:fEjI/gFP0DXxz5c4tRWyYEQpcNCVvMzjh62t0uKFk8U= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7 h1:pPhmvNKbgb9l5VHcPmMx9g+FHtRbY+ba2J6GefXQGEI= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.7/go.mod h1:OZU7QRvIYXhKry99PttkDTQyN8yCo8RzYjhIKHdQXoo= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.9 h1:aVVgQDwvAGq8Olf9nb+sQgSujPEybAg4ptxm+L2zisY= github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.9/go.mod h1:uCzvi36pXcTcGHwWXPHXkhaK9F4AjNo+IByRSv7BRe4= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.11 h1:KUHQows9JhDp+RJRs9KLN+ljsK5D+oLV13Wr/TwlSr4= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.11/go.mod h1:4kdmcGnKW4R9l2ddj6hNgKnJoxztjvJNCoI9eikMgvI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 h1:KiN5TPOLrEjbGCvdTQR4t0U4T87vVwALZ5Bg3jpMqPY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2/go.mod h1:dF2F6tXEOgmW5X1ZFO/EPtWrcm7XkW07KNcJUGNtt4s= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9 h1:cy8ahBJuhtM8GTTSyOkfy6WVPV1IE+SS5/wfXUYuulw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.9/go.mod h1:CZBXGLaJnEZI6EVNcPd7a6B5IC5cA/GkRWtu9fp3S6Y= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9 h1:A4SYk07ef04+vxZToz9LWvAXl9LW0NClpPpMsi31cz0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.9/go.mod h1:5jJcHuwDagxN+ErjQ3PU3ocf6Ylc/p9x+BLO/+X4iXw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 h1:IQup8Q6lorXeiA/rK72PeToWoWK8h7VAPgHNWdSrtgE= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2/go.mod h1:VITe/MdW6EMXPb0o0txu/fsonXbMHUU2OC2Qp7ivU4o= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.10.0/go.mod h1:ELltfl9ri0n4sZ/VjPZBgemNMd9mYIpCAuZhc7NP7l4= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8 h1:XKO0BswTDeZMLDBd/b5pCEZGttNXrzRUVtFvp2Ak/Vo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8/go.mod h1:N5tqZcYMM0N1PN7UQYJNWuGyO886OfnMhf/3MAbqMcI= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.32.8 h1:yOosUCdI/P+gfBd8uXk6lvZmrp7z2Xs8s1caIDP33lo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.32.8/go.mod h1:4sYs0Krug9vn4cfDly4ExdbXJRqqZZBVDJNtBHGxCpQ= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1 h1:Szwz1vpZkvfhFMJ0X5uUECgHeUmPAxk1UGqAVs/pARw= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.1/go.mod h1:b4wouGyJlzkr2HAvPrDGgYNp1EtmlXOkzhEOvl0c0FQ= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.3 h1:nEhZKd1JQ4EB1tekcqW1oIVpDC1ZFrjrp/cLC5MXjFQ= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.3/go.mod h1:q9vzW3Xr1KEXa8n4waHiFt1PrppNDlMymlYP+xpsFbY= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1 h1:AQurjazY9KPUxvq4EBN9Q3iWGaDrcqfpfSWtkP0Qy+g= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.8.1/go.mod h1:RiesWyLiePOOwyT5ySDupQosvbG+OTMv9pws/EhDu4U= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.20.10 h1:aK9uyT3Ua6UOmTMBYEM3sJHlnSO994eNZGagFlfLiOs= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.20.10/go.mod h1:S541uoWn3nWvo28EE8DnMbqZ5sZRAipVUPuL11V08Xw= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1 h1:jfkCLx62YWL6bSOkT7aEDKNAX3OwWomlThCxQNBPvbY= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.1/go.mod h1:dLPiMfhRZhblwOeKqdNde7K9jl/pMuIGCGAwC6vQOIo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5 h1:Cm77yt+/CV7A6DglkENsWA3H1hq8+4ItJnFKrhxHkvg= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5/go.mod h1:s2fYaueBuCnwv1XQn6T8TfShxJWusv5tWPMcL+GY6+g= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3 h1:r27/FnxLPixKBRIlslsvhqscBuMK8uysCYG9Kfgm098= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.3/go.mod h1:jqOFyN+QSWSoQC+ppyc4weiO8iNQXbzRbxDjQ1ayYd4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.4 h1:qOvCqaiLTc0MnIdZr0LbdtJKetiRscHxi+9XjjtlEAs= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.4/go.mod h1:3YxVsEoCNYOLIbdA+cCXSp1fom9hrhyB1DsCiYryCaQ= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.3.3/go.mod h1:zOyLMYyg60yyZpOCniAUuibWVqTU4TuLmMa/Wh4P+HA= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11 h1:e9AVb17H4x5FTE5KWIP5M1Du+9M86pS+Hw0lBUdN8EY= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11/go.mod h1:B90ZQJa36xo0ph9HsoteI1+r8owgQH/U1QNfqZQkj1Q= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.10 h1:+ijk29Q2FlKCinEzG6GE3IcOyBsmPNUmFq/L82pSyhI= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.10/go.mod h1:D9WZXFWtJD76gmV2ZciWcY8BJBFdCblqdfF9OmkrwVU= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14 h1:X1J0Kd17n1PeXeoArNXlvnKewCyMvhVQh7iNMy6oi3s= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.14/go.mod h1:VYMN7l7dxp6xtQRjqIau6d7QAbmPG+yJ75GtCy70f18= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16 h1:lhAX5f7KpgwyieXjbDnRTjPEUI0l3emSRyxXj1PXP8w= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.16/go.mod h1:AblAlCwvi7Q/SFowvckgN+8M3uFPlopSYeLlbNDArhA= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17 h1:HDJGz1jlV7RokVgTPfx1UHBHANC0N5Uk++xgyYgz5E0= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17/go.mod h1:5szDu6TWdRDytfDxUQVv2OYfpTQMKApVFyqpm+TcA98= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 h1:CKdUNKmuilw/KNmO2Q53Av8u+ZyXMC2M9aX8Z+c/gzg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2/go.mod h1:FgR1tCsn8C6+Hf+N5qkfrE4IXvUL1RgW87sunJ+5J4I= github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 h1:2IDmvSb86KT44lSg1uU4ONpzgWLOuApRl6Tg54mZ6Dk= @@ -81,21 +48,17 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.6.2/go.mod h1:KnIpszaIdwI33tmc/W/GGX github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZgUevSSJ4wxmyWXk= github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA= github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= -github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= -github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= +github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -103,10 +66,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= diff --git a/put.go b/put.go index 8676a01..4fd9cf2 100644 --- a/put.go +++ b/put.go @@ -11,7 +11,7 @@ import ( // See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html type Put struct { table Table - returnType string + returnType types.ReturnValue item Item subber @@ -54,16 +54,16 @@ func (p *Put) ConsumedCapacity(cc *ConsumedCapacity) *Put { // Run executes this put. func (p *Put) Run(ctx context.Context) error { - p.returnType = "NONE" - _, err := p.run(ctx) + p.returnType = types.ReturnValueNone + _, _, err := p.run(ctx) return err } // OldValue executes this put, unmarshaling the previous value into out. // Returns ErrNotFound is there was no previous value. func (p *Put) OldValue(ctx context.Context, out interface{}) error { - p.returnType = "ALL_OLD" - output, err := p.run(ctx) + p.returnType = types.ReturnValueAllOld + _, output, err := p.run(ctx) switch { case err != nil: return err @@ -73,12 +73,33 @@ func (p *Put) OldValue(ctx context.Context, out interface{}) error { return unmarshalItem(output.Attributes, out) } -func (p *Put) run(ctx context.Context) (output *dynamodb.PutItemOutput, err error) { +// CurrentValue executes this put. +// If successful, the return value `wrote` will be true, and the input item will be unmarshaled to `out`. +// +// If the put is unsuccessful because of a condition check failure, `wrote` will be false, the current value of the item will be unmarshaled to `out`, and `err` will be nil. +// +// If the put is unsuccessful for any other reason, `wrote` will be false and `err` will be non-nil. +// +// See also: [UnmarshalItemFromCondCheckFailed]. +func (p *Put) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) { + p.returnType = types.ReturnValueNone + item, _, err := p.run(ctx) + wrote = err == nil + if err != nil { + _, err = UnmarshalItemFromCondCheckFailed(err, out) + return + } + err = unmarshalItem(item, out) + return +} + +func (p *Put) run(ctx context.Context) (item Item, output *dynamodb.PutItemOutput, err error) { if p.err != nil { - return nil, p.err + return nil, nil, p.err } req := p.input() + item = req.Item p.table.db.retry(ctx, func() error { output, err = p.table.db.client.PutItem(ctx, req) p.cc.incRequests() @@ -94,12 +115,13 @@ func (p *Put) input() *dynamodb.PutItemInput { input := &dynamodb.PutItemInput{ TableName: &p.table.name, Item: p.item, - ReturnValues: types.ReturnValue(p.returnType), + ReturnValues: p.returnType, ExpressionAttributeNames: p.nameExpr, ExpressionAttributeValues: p.valueExpr, } if p.condition != "" { input.ConditionExpression = &p.condition + input.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld } if p.cc != nil { input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes @@ -114,13 +136,12 @@ func (p *Put) writeTxItem() (*types.TransactWriteItem, error) { input := p.input() item := &types.TransactWriteItem{ Put: &types.Put{ - TableName: input.TableName, - Item: input.Item, - ExpressionAttributeNames: input.ExpressionAttributeNames, - ExpressionAttributeValues: input.ExpressionAttributeValues, - ConditionExpression: input.ConditionExpression, - // TODO: add support when aws-sdk-go updates - // ReturnValuesOnConditionCheckFailure: aws.String(dynamodb.ReturnValuesOnConditionCheckFailureAllOld), + TableName: input.TableName, + Item: input.Item, + ExpressionAttributeNames: input.ExpressionAttributeNames, + ExpressionAttributeValues: input.ExpressionAttributeValues, + ConditionExpression: input.ConditionExpression, + ReturnValuesOnConditionCheckFailure: input.ReturnValuesOnConditionCheckFailure, }, } return item, nil diff --git a/put_test.go b/put_test.go index cbdd400..7251afa 100644 --- a/put_test.go +++ b/put_test.go @@ -69,10 +69,32 @@ func TestPut(t *testing.T) { } // putting the same item: this should fail - err = table.Put(newItem).If("attribute_not_exists(UserID)").If("attribute_not_exists('Time')").Run(ctx) - if !IsCondCheckFailed(err) { - t.Error("expected ConditionalCheckFailedException, not", err) - } + t.Run("UnmarshalItemFromCondCheckFailed", func(t *testing.T) { + err := table.Put(newItem).If("attribute_not_exists(UserID)").If("attribute_not_exists('Time')").Run(ctx) + if !IsCondCheckFailed(err) { + t.Error("expected ConditionalCheckFailedException, not", err) + } + var curr widget2 + if match, err := UnmarshalItemFromCondCheckFailed(err, &curr); !match || err != nil { + t.Error("bad match:", match, err) + } + if curr.Msg != newItem.Msg { + t.Errorf("bad cond check fail value. %#v ≠ %#v", curr, newItem) + } + }) + t.Run("CurrentValue", func(t *testing.T) { + var curr widget2 + wrote, err := table.Put(newItem).If("attribute_not_exists(UserID)").If("attribute_not_exists('Time')").CurrentValue(ctx, &curr) + if err != nil { + t.Fatal(err) + } + if wrote { + t.Error("wrote should be false") + } + if curr.Msg != newItem.Msg { + t.Errorf("bad cond check fail value. %#v ≠ %#v", curr, newItem) + } + }) } func TestPutAndQueryAWSEncoding(t *testing.T) { diff --git a/tx_test.go b/tx_test.go index d44d709..f73ac8a 100644 --- a/tx_test.go +++ b/tx_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "sort" "sync" "testing" "time" @@ -138,6 +139,33 @@ func TestTx(t *testing.T) { t.Error(err) } + t.Run("UnmarshalItemsFromTxCondCheckFailed", func(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")) + err := tx.Run(ctx) + if err == nil { + t.Fatal("expected error") + } + var badItems []widget + match, err := UnmarshalItemsFromTxCondCheckFailed(err, &badItems) + if !match { + t.Error("error doesn't match", err) + } + if err != nil { + t.Error(err) + } + if len(badItems) != 2 { + t.Error("wrong amount of bad items:", len(badItems), badItems) + } + sort.Slice(badItems, func(i, j int) bool { + return badItems[i].Time.Before(badItems[j].Time) + }) + if !badItems[0].Time.Equal(date1) || !badItems[1].Time.Equal(date2) { + t.Error("wrong unmarshaled values:", badItems) + } + }) + // Delete tx = testDB.WriteTx() tx.Delete(table.Delete("UserID", widget1.UserID).Range("Time", widget1.Time).If("Msg = ?", widget1.Msg)) diff --git a/update.go b/update.go index 1942efb..24bec84 100644 --- a/update.go +++ b/update.go @@ -14,7 +14,7 @@ import ( // See: http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html type Update struct { table Table - returnType string + returnType types.ReturnValue hashKey string hashValue types.AttributeValue @@ -288,7 +288,7 @@ func (u *Update) ConsumedCapacity(cc *ConsumedCapacity) *Update { // Run executes this update. func (u *Update) Run(ctx context.Context) error { - u.returnType = "NONE" + u.returnType = types.ReturnValueNone _, err := u.run(ctx) return err } @@ -296,7 +296,7 @@ func (u *Update) Run(ctx context.Context) error { // Value executes this update, encoding out with the new value after the update. // This is equivalent to ReturnValues = ALL_NEW in the DynamoDB API. func (u *Update) Value(ctx context.Context, out interface{}) error { - u.returnType = "ALL_NEW" + u.returnType = types.ReturnValueAllNew output, err := u.run(ctx) if err != nil { return err @@ -307,7 +307,7 @@ func (u *Update) Value(ctx context.Context, out interface{}) error { // OldValue executes this update, encoding out with the old value before the update. // This is equivalent to ReturnValues = ALL_OLD in the DynamoDB API. func (u *Update) OldValue(ctx context.Context, out interface{}) error { - u.returnType = "ALL_OLD" + u.returnType = types.ReturnValueAllOld output, err := u.run(ctx) if err != nil { return err @@ -318,7 +318,7 @@ func (u *Update) OldValue(ctx context.Context, out interface{}) error { // OnlyUpdatedValue executes this update, encoding out with only with new values of the attributes that were changed. // This is equivalent to ReturnValues = UPDATED_NEW in the DynamoDB API. func (u *Update) OnlyUpdatedValue(ctx context.Context, out interface{}) error { - u.returnType = "UPDATED_NEW" + u.returnType = types.ReturnValueUpdatedNew output, err := u.run(ctx) if err != nil { return err @@ -329,7 +329,7 @@ func (u *Update) OnlyUpdatedValue(ctx context.Context, out interface{}) error { // OnlyUpdatedOldValue executes this update, encoding out with only with old values of the attributes that were changed. // This is equivalent to ReturnValues = UPDATED_OLD in the DynamoDB API. func (u *Update) OnlyUpdatedOldValue(ctx context.Context, out interface{}) error { - u.returnType = "UPDATED_OLD" + u.returnType = types.ReturnValueUpdatedOld output, err := u.run(ctx) if err != nil { return err @@ -337,6 +337,26 @@ func (u *Update) OnlyUpdatedOldValue(ctx context.Context, out interface{}) error return unmarshalItem(output.Attributes, out) } +// CurrentValue executes this update. +// If successful, the return value `wrote` will be true, and the input item will be unmarshaled to `out`. +// +// If the update is unsuccessful because of a condition check failure, `wrote` will be false, the current value of the item will be unmarshaled to `out`, and `err` will be nil. +// +// If the update is unsuccessful for any other reason, `wrote` will be false and `err` will be non-nil. +// +// See also: [UnmarshalItemFromCondCheckFailed]. +func (u *Update) CurrentValue(ctx context.Context, out interface{}) (wrote bool, err error) { + u.returnType = types.ReturnValueAllNew + output, err := u.run(ctx) + if err != nil { + if ok, err := UnmarshalItemFromCondCheckFailed(err, out); ok { + return false, err + } + return false, err + } + return true, unmarshalItem(output.Attributes, out) +} + func (u *Update) run(ctx context.Context) (*dynamodb.UpdateItemOutput, error) { if u.err != nil { return nil, u.err @@ -363,10 +383,11 @@ func (u *Update) updateInput() *dynamodb.UpdateItemInput { UpdateExpression: u.updateExpr(), ExpressionAttributeNames: u.nameExpr, ExpressionAttributeValues: u.valueExpr, - ReturnValues: types.ReturnValue(u.returnType), + ReturnValues: u.returnType, } if u.condition != "" { input.ConditionExpression = &u.condition + input.ReturnValuesOnConditionCheckFailure = types.ReturnValuesOnConditionCheckFailureAllOld } if u.cc != nil { input.ReturnConsumedCapacity = types.ReturnConsumedCapacityIndexes @@ -381,13 +402,13 @@ func (u *Update) writeTxItem() (*types.TransactWriteItem, error) { input := u.updateInput() item := &types.TransactWriteItem{ Update: &types.Update{ - TableName: input.TableName, - Key: input.Key, - UpdateExpression: input.UpdateExpression, - ExpressionAttributeNames: input.ExpressionAttributeNames, - ExpressionAttributeValues: input.ExpressionAttributeValues, - ConditionExpression: input.ConditionExpression, - // TODO: return values + TableName: input.TableName, + Key: input.Key, + UpdateExpression: input.UpdateExpression, + ExpressionAttributeNames: input.ExpressionAttributeNames, + ExpressionAttributeValues: input.ExpressionAttributeValues, + ConditionExpression: input.ConditionExpression, + ReturnValuesOnConditionCheckFailure: input.ReturnValuesOnConditionCheckFailure, }, } return item, nil diff --git a/update_test.go b/update_test.go index fce154e..e2f1f15 100644 --- a/update_test.go +++ b/update_test.go @@ -154,15 +154,19 @@ func TestUpdate(t *testing.T) { } // send an update with a failing condition - err = table.Update("UserID", item.UserID). + var curr widget2 + wrote, err := table.Update("UserID", item.UserID). Range("Time", item.Time). Set("Msg", "shouldn't happen"). Add("Count", 1). If("'Count' > ?", 100). If("(MeaningOfLife = ?)", 42). - Value(ctx, &result) - if !IsCondCheckFailed(err) { - t.Error("expected ConditionalCheckFailedException, not", err) + CurrentValue(ctx, &curr) + if wrote { + t.Error("wrote should be false") + } + if curr.Msg != "this shouldn't be seen" { + t.Errorf("bad result. %v", curr.Msg) } }