guregu/dynamo

repository·master·Indexed 23 days ago

https://github.com/guregu/dynamo

An expressive DynamoDB client for Go that integrates with the official AWS SDK v2. It provides a fluent API for common operations such as Put, Get, Scan, and Delete, while simplifying expression writing and struct mapping via the `dynamo` struct tag. The library supports batch operations (BatchGet and BatchWrite), custom projections, and integration with DynamoDB Local.

Tokens
14.1K
Snippets
22
Records
108
Agent score
79%

What's inside guregu/dynamo

  1. Configure struct tags for DynamoDB mapping

    master

    Dynamo uses the dynamo struct tag to map Go fields to DynamoDB attributes. The format is dynamo:"attributeName,option1,option2".

    Common Options:

    • Renaming: dynamo:"other_name" changes the attribute name.
    • Omission: dynamo:"-" ignores the field. Fields starting with lowercase letters are also ignored.
    • Sets: dynamo:",set" marshals slices or maps as DynamoDB sets. Supported types: []T, map[T]struct{}, and map[T]bool.
    • omitempty: dynamo:",omitempty" omits the field if it has a zero value. Supports the isZeroer interface (IsZero() bool).
    • omitemptyelem: dynamo:",omitemptyelem" omits empty values inside slices.
    • allowempty: dynamo:",allowempty" overrides automatic omission of empty strings, sets, or structs.
    • null: dynamo:",null" forces empty/nil values to marshal as the DynamoDB NULL type.
    • unixtime: dynamo:",unixtime" marshals time.Time as a Unix timestamp (useful for TTL).
  2. Define tables and indexes using struct tags

    master

    You can specify the schema for creating tables (including Hash keys, Range keys, and Indexes) directly in your Go structs using dynamo, index, and localIndex tags.

    type UserAction struct {
    	UserID string    `dynamo:"ID,hash" index:"Seq-ID-index,range"` 
    	Time   time.Time `dynamo:",range"` 
    	Seq    int64     `localIndex:"ID-Seq-index,range" index:"Seq-ID-index,hash"` 
    	UUID   string    `index:"UUID-index,hash"` 
    }
  3. Quickstart with dynamo

    master

    To use dynamo, import github.com/guregu/dynamo/v2 and initialize it using an AWS SDK v2 configuration. You can then interact with DynamoDB tables using a fluent API for operations like Put, Get, Scan, and Delete.

    package main
    
    import (
    	"context"
    	"log"
    
    	"github.com/aws/aws-sdk-go-v2/config"
    	"github.com/guregu/dynamo/v2"
    )
    
    type widget struct {
    	UserID int       `dynamo:"UserID"` 
    	Time   time.Time `dynamo:"Time"` 
    	Msg    string    `dynamo:"Message"` 
    }
    
    func main() {
    	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(context.TODO())
    
    	// get the same item
    	var result widget
    	err = table.Get("UserID", w.UserID).
    		Range("Time", dynamo.Equal, w.Time).
    		One(context.TODO(), &result)
    }
  4. Use AWS SDK v2 encoding with dynamo.AWSEncoding

    master

    If you need to use the official dynamodbattribute encoding (using dynamodbav tags), you must wrap your objects with dynamo.AWSEncoding.

    Note: When retrieving an item, you must pass a pointer to AWSEncoding to the One method.

    // Notice the use of the dynamodbav struct tag
    type book struct {
    	ID    int    `dynamodbav:"id"` 
    	Title string `dynamodbav:"title"` 
    }
    
    // Putting an item
    err := db.Table("Books").Put(dynamo.AWSEncoding(book{
    	ID:    42,
    	Title: "Principia Discordia",
    })).Run(ctx)
    
    // When getting an item you MUST pass a pointer to AWSEncoding!
    var someBook book
    err := db.Table("Books").Get("ID", 555).One(ctx, dynamo.AWSEncoding(&someBook))
  5. Use Expressions for filtering and conditions

    master

    DynamoDB expressions allow you to filter results in queries/scans and add conditions to Put or Delete operations.

    • Attribute Names: Use single quotes ('') to escape reserved words or use dollar signs ($) as placeholders for attribute names.
    • Attribute Values: Use question marks (?) as placeholders for values.

    Example usage:

    • Filter("'Date' >= ?", lastUpdate): Escapes reserved word 'Date'.
    • If("Score <= ? AND begins_with($, ?)", cutoff, "Name", "G"): Uses both name and value placeholders.
    // 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(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(ctx)
    
    // Put a new item, only if it doesn't already exist.
    table.Put(item{ID: 42}).If("attribute_not_exists(ID)").Run(ctx)
  6. Configure retries for transaction conflicts

    master

    In v2, dynamo relies on the AWS SDK for retries. By default, canceled transactions (conflicting transactions) are not retried. To enable automatic retrying for transaction conflicts, configure the AWS SDK Retryer with dynamo.RetryTxConflicts.

    import (
    	"context"
    	"log"
    
    	"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"
    )
    
    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
    }
  7. Run integration tests against DynamoDB Local

    master

    To run integration tests against a local instance (e.g., via Docker), set the following environment variables and run go test:

    # Use Docker to run DynamoDB local on port 8880
    docker compose -f '.github/docker-compose.yml' up -d
    
    # Run the tests with a fresh table
    DYNAMO_TEST_ENDPOINT='http://localhost:8880' \
    	DYNAMO_TEST_REGION='local' \
    	DYNAMO_TEST_TABLE='TestDB-%' \
    	AWS_ACCESS_KEY_ID='dummy' \
    	AWS_SECRET_ACCESS_KEY='dummy' \
    	AWS_REGION='local' \
    	go test -v -race ./... -cover -coverpkg=./...
  8. Use Iterators for paginated results

    master

    The Iter interface is used for traversing result sets. There are three specialized types of iterators:

    • Iter: The base interface for sequential traversal using Next(ctx, out) and Err().
    • PagingIter: An iterator that supports pagination. It provides LastEvaluatedKey(ctx) which returns a PagingKey. You can pass this key to StartFrom in subsequent Query or Scan calls to resume pagination.
    • ParallelIter: An iterator representing combined results from multiple parallel segments. It provides LastEvaluatedKeys(ctx) which returns a slice of PagingKey objects, one for each parallel segment.
  9. How parallel scans work with IterParallel

    master

    To speed up large scans, use IterParallel(ctx context.Context, segments int). This method automatically splits the table into the specified number of segments and runs them in parallel using goroutines.

    Key behaviors:

    • Canceling the provided ctx will cancel all underlying segment scans.
    • It returns a ParallelIter which can be used to iterate over items across all segments as they arrive.
    • It is more efficient than manual segmentation for large datasets.
  10. Handle condition check failures and retrieve items

    master

    When using conditional puts, you can control whether the item that caused the condition check failure is included in the error response using IncludeItemInCondCheckFail(bool).

    • If enabled is true, the item is included in the error, which can be extracted using UnmarshalItemFromCondCheckFailed (for single puts) or UnmarshalItemsFromTxCondCheckFailed (for transactions).
    • If false, the item is not included.

    This is particularly useful when using CurrentValue, which uses ReturnValuesOnConditionCheckFailureAllOld internally to allow you to inspect the existing item when a write fails due to a condition.

  11. How to resume a scan using LastEvaluatedKey

    master
    DynamoDB scans are paginated. To continue a scan that was interrupted or limited, use the LastEvaluatedKey returned by the iterator or the AllWithLastEvaluatedKey method. This key can then be passed to StartFrom(key PagingKey) on a new Scan request.
  12. Inspect Global and Local Secondary Indexes

    master

    The Description struct includes GSI (Global Secondary Indexes) and LSI (Local Secondary Indexes) as slices of Index objects.

    An Index object contains:

    • Name and ARN.
    • Status and Backfilling (for GSIs).
    • Local: A boolean that is true for LSIs and false for GSIs.
    • HashKey / RangeKey and their respective KeyTypes.
    • HashKeys / RangeKeys: Used for composite GSIs with multiple keys.
    • Throughput: The provisioned throughput for the index.
    • ProjectionType: The type of index projection.
    • ProjectionAttribs: The non-key attributes included in the projection (if ProjectionType is IncludeProjection).