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

dynamo v2: featuring aws-sdk-go-v2 #232

Merged
merged 36 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0d038a9
Feat/dynamo sdk v2 (#2)
niltonkummer Dec 10, 2021
4304bbf
Remove unnecessary parenthesis
irohirokid Oct 13, 2022
2b00a6b
Fix problem of decoding AttributeValueMemberM
irohirokid Oct 13, 2022
2062008
Remove unnecessary code
irohirokid Oct 15, 2022
4da56f4
Remove unnecessary blank line
irohirokid Oct 18, 2022
0e4bb5e
Kill awserr
irohirokid Oct 26, 2022
d109af1
Recover unwrapping AWSEncoding wrapper
irohirokid Oct 28, 2022
a553025
int32 Query/Scan limit
irohirokid Oct 31, 2022
6aadb24
Improvement of init in test
irohirokid Nov 1, 2022
8c29226
Use standard context module
irohirokid Nov 1, 2022
c2f91d7
Remove unnecessary blank lines in import block
irohirokid Nov 2, 2022
1afde07
Update expressions making blank slice
irohirokid Nov 7, 2022
9552dd0
Change repo
irohirokid Nov 9, 2022
63eda64
Merge irohiroki:aws-sdk-v2-compliant into v2-dev
guregu Jan 5, 2024
e74a218
fix endpoint for tests
guregu Jan 5, 2024
0064f3b
hmm
guregu Jan 5, 2024
03408e0
remove spurious dep
guregu Jan 5, 2024
801336f
fix retrying settings, remove old sdk references
guregu Jan 27, 2024
91db943
fix retrying tests
guregu Jan 27, 2024
ef69352
change LastEvaluatedKey signature
guregu Jan 27, 2024
2b8cde0
rename KMSMasterKeyArn -> KMSMasterKeyARN
guregu Jan 27, 2024
9b9e3cb
remove non-context methods
guregu Jan 27, 2024
a749b0c
Merge branch 'master' into v2-dev
guregu May 4, 2024
ec1651c
remove custom retrying stuff in favor of aws impl
guregu May 4, 2024
681c46a
use int instead of int32 in APIs
guregu May 4, 2024
7e34253
Merge branch 'master' into v2-dev
guregu May 4, 2024
95d7bd1
Merge branch 'master' into v2-dev
guregu May 5, 2024
23cb3df
Merge branch 'master' into v2-dev
guregu May 20, 2024
8728b18
move table description cache to *DB
guregu Jun 15, 2024
7ba9ed2
update README for v2
guregu Jun 15, 2024
ed12b7d
add RetryTx for old tx retrying behavior
guregu Jun 15, 2024
35807f6
add some examples
guregu Jun 15, 2024
ae8d3cc
update README migration tips
guregu Jun 15, 2024
b910e5f
touch up the README
guregu Jun 15, 2024
840a2a6
bump versions
guregu Jun 15, 2024
756ca4a
rename RetryTx to RetryTxConflicts
guregu Jun 15, 2024
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
101 changes: 59 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
## dynamo [![GoDoc](https://godoc.org/github.com/guregu/dynamo?status.svg)](https://godoc.org/github.com/guregu/dynamo)
`import "github.com/guregu/dynamo"`
## dynamo [![GoDoc](https://godoc.org/github.com/guregu/dynamo/v2?status.svg)](https://godoc.org/github.com/guregu/dynamo/v2)
`import "github.com/guregu/dynamo/v2"`

dynamo is an expressive [DynamoDB](https://aws.amazon.com/dynamodb/) client for Go, with an easy but powerful API. dynamo integrates with the official [AWS SDK](https://github.com/aws/aws-sdk-go/).
dynamo is an expressive [DynamoDB](https://aws.amazon.com/dynamodb/) client for Go, with an easy but powerful API. dynamo integrates with the official [AWS SDK v2](https://github.com/aws/aws-sdk-go-v2/).

This library is stable and versioned with Go modules.

> [!TIP]
> dynamo v2 is finally released! See [**v2 Migration**](#migrating-from-v1) for tips on migrating from dynamo v1.
>
> For dynamo v1, which uses [aws-sdk-go v1](https://github.com/aws/aws-sdk-go/), see: [**dynamo v1 documentation**](https://pkg.go.dev/github.com/guregu/dynamo).

### Example

```go
package dynamo

import (
"time"
"context"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/guregu/dynamo"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/guregu/dynamo/v2"
)

// Use struct tags much like the standard JSON library,
Expand All @@ -34,27 +41,30 @@ type widget struct {


func main() {
sess := session.Must(session.NewSession())
db := dynamo.New(sess, &aws.Config{Region: aws.String("us-west-2")})
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1"))
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
db := dynamo.New(cfg)
table := db.Table("Widgets")

// put item
w := widget{UserID: 613, Time: time.Now(), Msg: "hello"}
err := table.Put(w).Run()
err = table.Put(w).Run(ctx)

// get the same item
var result widget
err = table.Get("UserID", w.UserID).
Range("Time", dynamo.Equal, w.Time).
One(&result)
One(ctx, &result)

// get all items
var results []widget
err = table.Scan().All(&results)
err = table.Scan().All(ctx, &results)

// use placeholders in filter expressions (see Expressions section below)
var filtered []widget
err = table.Scan().Filter("'Count' > ?", 10).All(&filtered)
err = table.Scan().Filter("'Count' > ?", 10).All(ctx, &filtered)
}
```

Expand All @@ -71,14 +81,14 @@ Please see the [DynamoDB reference on expressions](http://docs.aws.amazon.com/am
```go
// Using single quotes to escape a reserved word, and a question mark as a value placeholder.
// Finds all items whose date is greater than or equal to lastUpdate.
table.Scan().Filter("'Date' >= ?", lastUpdate).All(&results)
table.Scan().Filter("'Date' >= ?", lastUpdate).All(ctx, &results)

// Using dollar signs as a placeholder for attribute names.
// Deletes the item with an ID of 42 if its score is at or below the cutoff, and its name starts with G.
table.Delete("ID", 42).If("Score <= ? AND begins_with($, ?)", cutoff, "Name", "G").Run()
table.Delete("ID", 42).If("Score <= ? AND begins_with($, ?)", cutoff, "Name", "G").Run(ctx)

// Put a new item, only if it doesn't already exist.
table.Put(item{ID: 42}).If("attribute_not_exists(ID)").Run()
table.Put(item{ID: 42}).If("attribute_not_exists(ID)").Run(ctx)
```

### Encoding support
Expand Down Expand Up @@ -177,42 +187,38 @@ This creates a table with the primary hash key ID and range key Time. It creates

### Retrying

Requests that fail with certain errors (e.g. `ThrottlingException`) are [automatically retried](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.RetryAndBackoff).
Methods that take a `context.Context` will retry until the context is canceled.
Methods without a context will use the `RetryTimeout` global variable, which can be changed; using context is recommended instead.

#### Limiting or disabling retrying
As of v2, dynamo relies on the AWS SDK for retrying. See: [**Retries and Timeouts documentation**](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/retries-timeouts/) for information about how to configure its behavior.

The maximum number of retries can be configured via the `MaxRetries` field in the `*aws.Config` passed to `dynamo.New()`. A value of `0` will disable retrying. A value of `-1` means unlimited and is the default (however, context or `RetryTimeout` will still apply).
By default, canceled transactions (i.e. errors from conflicting transactions) will not be retried. To get automatic retrying behavior like in v1, use [`dynamo.RetryTxConflicts`](https://godoc.org/github.com/guregu/dynamo/v2#RetryTxConflicts).

```go
db := dynamo.New(session, &aws.Config{
MaxRetries: aws.Int(0), // disables automatic retrying
})
```

#### Custom retrying logic

If a custom [`request.Retryer`](https://pkg.go.dev/github.com/aws/aws-sdk-go/aws/request#Retryer) is set via the `Retryer` field in `*aws.Config`, dynamo will delegate retrying entirely to it, taking precedence over other retrying settings. This allows you to have full control over all aspects of retrying.
import (
"context"
"log"

Example using [`client.DefaultRetryer`](https://pkg.go.dev/github.com/aws/aws-sdk-go/aws/client#DefaultRetryer):
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/retry"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/guregu/dynamo/v2"
)

```go
retryer := client.DefaultRetryer{
NumMaxRetries: 10,
MinThrottleDelay: 500 * time.Millisecond,
MaxThrottleDelay: 30 * time.Second,
func main() {
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRetryer(func() aws.Retryer {
return retry.NewStandard(dynamo.RetryTxConflicts)
}))
if err != nil {
log.Fatal(err)
}
db := dynamo.New(cfg)
// use db
}
db := dynamo.New(session, &aws.Config{
Retryer: retryer,
})
```

### Compatibility with the official AWS library

dynamo has been in development before the official AWS libraries were stable. We use a different encoder and decoder than the [dynamodbattribute](https://godoc.org/github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute) package. dynamo uses the `dynamo` struct tag instead of the `dynamodbav` struct tag, and we also prefer to automatically omit invalid values such as empty strings, whereas the dynamodbattribute package substitutes null values for them. Items that satisfy the [`dynamodbattribute.(Un)marshaler`](https://godoc.org/github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute#Marshaler) interfaces are compatibile with both libraries.
dynamo has been in development before the official AWS libraries were stable. We use a different encoder and decoder than the [dynamodbattribute](https://pkg.go.dev/github.com/jviney/aws-sdk-go-v2/service/dynamodb/dynamodbattribute) package. dynamo uses the `dynamo` struct tag instead of the `dynamodbav` struct tag, and we also prefer to automatically omit invalid values such as empty strings, whereas the dynamodbattribute package substitutes null values for them. Items that satisfy the [`dynamodbattribute.(Un)marshaler`](https://pkg.go.dev/github.com/jviney/aws-sdk-go-v2/service/dynamodb/dynamodbattribute#Marshaler) interfaces are compatibile with both libraries.

In order to use dynamodbattribute's encoding facilities, you must wrap objects passed to dynamo with [`dynamo.AWSEncoding`](https://godoc.org/github.com/guregu/dynamo#AWSEncoding). Here is a quick example:
In order to use dynamodbattribute's encoding facilities, you must wrap objects passed to dynamo with [`dynamo.AWSEncoding`](https://godoc.org/github.com/guregu/dynamo/v2#AWSEncoding). Here is a quick example:

```go
// Notice the use of the dynamodbav struct tag
Expand All @@ -224,12 +230,23 @@ type book struct {
err := db.Table("Books").Put(dynamo.AWSEncoding(book{
ID: 42,
Title: "Principia Discordia",
})).Run()
})).Run(ctx)
// When getting an item you MUST pass a pointer to AWSEncoding!
var someBook book
err := db.Table("Books").Get("ID", 555).One(dynamo.AWSEncoding(&someBook))
err := db.Table("Books").Get("ID", 555).One(ctx, dynamo.AWSEncoding(&someBook))
```

### Migrating from v1

The API hasn't changed much from v1 to v2. Here are some migration tips:

- All request methods now take a [context](https://go.dev/blog/context) as their first argument.
- Retrying relies on the AWS SDK configuration, see: [Retrying](#retrying).
- Transactions won't retry TransactionCanceled responses by default anymore, make sure you configure that if you need it.
- Arguments that took `int64` (such as in `Query.Limit`) now take `int` instead.
- [Compatibility with the official AWS library](#compatibility-with-the-official-aws-library) uses v2 interfaces instead of v1.
- `KMSMasterKeyArn` renamed to `KMSMasterKeyARN`.

### Integration tests

By default, tests are run in offline mode. In order to run the integration tests, some environment variables need to be set.
Expand Down
108 changes: 52 additions & 56 deletions attr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"fmt"
"strconv"

"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

// Item is a type alias for the raw DynamoDB item type.
type Item = map[string]*dynamodb.AttributeValue
type Item = map[string]types.AttributeValue

type shapeKey byte

Expand All @@ -31,114 +31,110 @@ const (
shapeInvalid shapeKey = 0
)

func shapeOf(av *dynamodb.AttributeValue) shapeKey {
func shapeOf(av types.AttributeValue) shapeKey {
if av == nil {
return shapeInvalid
}
switch {
case av.B != nil:
switch av.(type) {
case *types.AttributeValueMemberB:
return shapeB
case av.BS != nil:
case *types.AttributeValueMemberBS:
return shapeBS
case av.BOOL != nil:
case *types.AttributeValueMemberBOOL:
return shapeBOOL
case av.N != nil:
case *types.AttributeValueMemberN:
return shapeN
case av.S != nil:
case *types.AttributeValueMemberS:
return shapeS
case av.L != nil:
case *types.AttributeValueMemberL:
return shapeL
case av.NS != nil:
case *types.AttributeValueMemberNS:
return shapeNS
case av.SS != nil:
case *types.AttributeValueMemberSS:
return shapeSS
case av.M != nil:
case *types.AttributeValueMemberM:
return shapeM
case av.NULL != nil:
case *types.AttributeValueMemberNULL:
return shapeNULL
}
return shapeAny
}

// av2iface converts an av into interface{}.
func av2iface(av *dynamodb.AttributeValue) (interface{}, error) {
switch {
case av.B != nil:
return av.B, nil
case av.BS != nil:
return av.BS, nil
case av.BOOL != nil:
return *av.BOOL, nil
case av.N != nil:
return strconv.ParseFloat(*av.N, 64)
case av.S != nil:
return *av.S, nil
case av.L != nil:
list := make([]interface{}, 0, len(av.L))
for _, item := range av.L {
func av2iface(av types.AttributeValue) (interface{}, error) {
switch v := av.(type) {
case *types.AttributeValueMemberB:
return v.Value, nil
case *types.AttributeValueMemberBS:
return v.Value, nil
case *types.AttributeValueMemberBOOL:
return v.Value, nil
case *types.AttributeValueMemberN:
return strconv.ParseFloat(v.Value, 64)
case *types.AttributeValueMemberS:
return v.Value, nil
case *types.AttributeValueMemberL:
list := make([]interface{}, 0, len(v.Value))
for _, item := range v.Value {
iface, err := av2iface(item)
if err != nil {
return nil, err
}
list = append(list, iface)
}
return list, nil
case av.NS != nil:
set := make([]float64, 0, len(av.NS))
for _, n := range av.NS {
f, err := strconv.ParseFloat(*n, 64)
case *types.AttributeValueMemberNS:
set := make([]float64, 0, len(v.Value))
for _, n := range v.Value {
f, err := strconv.ParseFloat(n, 64)
if err != nil {
return nil, err
}
set = append(set, f)
}
return set, nil
case av.SS != nil:
set := make([]string, 0, len(av.SS))
for _, s := range av.SS {
set = append(set, *s)
}
return set, nil
case av.M != nil:
m := make(map[string]interface{}, len(av.M))
for k, v := range av.M {
case *types.AttributeValueMemberSS:
return v.Value, nil
case *types.AttributeValueMemberM:
m := make(map[string]interface{}, len(v.Value))
for k, v := range v.Value {
iface, err := av2iface(v)
if err != nil {
return nil, err
}
m[k] = iface
}
return m, nil
case av.NULL != nil:
case *types.AttributeValueMemberNULL:
return nil, nil
}
return nil, fmt.Errorf("dynamo: unsupported AV: %#v", *av)
return nil, fmt.Errorf("dynamo: unsupported AV: %#v", av)
}

func avTypeName(av *dynamodb.AttributeValue) string {
func avTypeName(av types.AttributeValue) string {
if av == nil {
return "<nil>"
}
switch {
case av.B != nil:
switch av.(type) {
case *types.AttributeValueMemberB:
return "binary"
case av.BS != nil:
case *types.AttributeValueMemberBS:
return "binary set"
case av.BOOL != nil:
case *types.AttributeValueMemberBOOL:
return "boolean"
case av.N != nil:
case *types.AttributeValueMemberN:
return "number"
case av.S != nil:
case *types.AttributeValueMemberS:
return "string"
case av.L != nil:
case *types.AttributeValueMemberL:
return "list"
case av.NS != nil:
case *types.AttributeValueMemberNS:
return "number set"
case av.SS != nil:
case *types.AttributeValueMemberSS:
return "string set"
case av.M != nil:
case *types.AttributeValueMemberM:
return "map"
case av.NULL != nil:
case *types.AttributeValueMemberNULL:
return "null"
}
return "<empty>"
Expand Down
Loading