Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 14 additions & 1 deletion get_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,32 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)

type GetItemInput = dynamodb.GetItemInput

// Function Struct for providing option input params for GetItem
type GetItemOptions func(g *dynamodb.GetItemInput)

/*
Used to get a db record from dynamodb given a partition key and sort key
@param partitionKey the partition key of the record
@param sortKey the sort key of the record
@param result the result of the query written to given memory reference
@param opts optional GetItemOptions for configuring the request
@return error, true if the record was found, false otherwise
*/
func (t *Client) GetItem(ctx context.Context, pk Attribute, sk Attribute, out interface{}) (err error, found bool) {
func (t *Client) GetItem(ctx context.Context, pk Attribute, sk Attribute, out interface{}, opts ...GetItemOptions) (err error, found bool) {
input := &dynamodb.GetItemInput{
TableName: &t.TableName,
Key: t.NewKeys(pk, sk),
}

// Apply optional function parameters
if len(opts) > 0 {
for _, opt := range opts {
opt(input)
}
}

resp, err := t.client.GetItem(ctx, input)
if err != nil {
// fixme: remove logs or log based on log level
Expand Down
2 changes: 1 addition & 1 deletion interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type TransactionAPI interface {
}

type ReadAPI interface {
GetItem(ctx context.Context, pk, sk Attribute, out interface{}) (error, bool)
GetItem(ctx context.Context, pk, sk Attribute, out interface{}, opts ...GetItemOptions) (error, bool)
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value order (error, bool) in the interface is inconsistent with Go conventions where error should be the last return value. Consider changing to (bool, error) for better consistency with Go idioms.

Suggested change
GetItem(ctx context.Context, pk, sk Attribute, out interface{}, opts ...GetItemOptions) (error, bool)
GetItem(ctx context.Context, pk, sk Attribute, out interface{}, opts ...GetItemOptions) (bool, error)

Copilot uses AI. Check for mistakes.
BatchGetItems(ctx context.Context, input []AttributeRecord, out interface{}) error
}

Expand Down
24 changes: 24 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dynago

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

// WithConsistentRead enables strongly consistent read for Query operations
// See documentation https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html
// Note that the cost for strongly consistent reads are double of eventual consistent reads
func WithConsistentRead() QueryOptions {
return func(q *dynamodb.QueryInput) {
q.ConsistentRead = aws.Bool(true)
}
}

// WithConsistentReadItem enables strongly consistent read for GetItem operations
// See documentation https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html
// Note that the cost for strongly consistent reads are double of eventual consistent reads
func WithConsistentReadItem() GetItemOptions {
return func(g *dynamodb.GetItemInput) {
g.ConsistentRead = aws.Bool(true)
}
}
94 changes: 94 additions & 0 deletions tests/getitem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package tests

import (
"context"
"reflect"
"testing"

"github.com/oolio-group/dynago"
)

func TestGetItemWithConsistentRead(t *testing.T) {
testCases := []struct {
title string
pk dynago.Attribute
sk dynago.Attribute
opts []dynago.GetItemOptions
source *User
expectedFound bool
expectConsistentRead bool
}{
{
title: "get item with consistent read enabled",
pk: dynago.StringValue("users#consistent_getitem"),
sk: dynago.StringValue("user#consistent"),
opts: []dynago.GetItemOptions{dynago.WithConsistentReadItem()},
source: &User{
Id: "consistent_user",
Pk: "users#consistent_getitem",
Sk: "user#consistent",
},
expectedFound: true,
expectConsistentRead: true,
},
{
title: "get item without consistent read (default eventual consistency)",
pk: dynago.StringValue("users#eventual_getitem"),
sk: dynago.StringValue("user#eventual"),
opts: []dynago.GetItemOptions{},
source: &User{
Id: "eventual_user",
Pk: "users#eventual_getitem",
Sk: "user#eventual",
},
expectedFound: true,
expectConsistentRead: false,
},
{
title: "get item not found with consistent read",
pk: dynago.StringValue("users#notfound"),
sk: dynago.StringValue("user#notfound"),
opts: []dynago.GetItemOptions{dynago.WithConsistentReadItem()},
source: nil,
expectedFound: false,
expectConsistentRead: true,
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

table := prepareTable(t)
ctx := context.TODO()

// prepare the table, write test sample data if provided
if tc.source != nil {
err := table.PutItem(ctx, tc.pk, tc.sk, tc.source)
if err != nil {
t.Fatalf("unexpected error setting up test data: %s", err)
}
}

var out User
err, found := table.GetItem(ctx, tc.pk, tc.sk, &out, tc.opts...)
if err != nil {
t.Fatalf("unexpected error %s", err)
}

if found != tc.expectedFound {
t.Errorf("expected found to be %v; got %v", tc.expectedFound, found)
}

if tc.expectedFound && tc.source != nil {
if !reflect.DeepEqual(*tc.source, out) {
t.Errorf("expected GetItem to return %v; got %v", *tc.source, out)
}
}

// Note: We can't directly verify that ConsistentRead was set in the actual DynamoDB request
// because that would require mocking the AWS client. The test verifies that the function
// can be called without error and returns expected results.
})
}
}
140 changes: 140 additions & 0 deletions tests/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,143 @@ func TestQueryPagination(t *testing.T) {
})
}
}

func TestQueryWithConsistentRead(t *testing.T) {
testCases := []struct {
title string
condition string
keys map[string]types.AttributeValue
opts []dynago.QueryOptions
source []User
expected []User
expectConsistentRead bool
}{
{
title: "query with consistent read enabled",
condition: "pk = :pk",
keys: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "users#consistent_test"},
},
opts: []dynago.QueryOptions{dynago.WithConsistentRead()},
source: []User{
{
Id: "1",
Pk: "users#consistent_test",
Sk: "user#1",
},
{
Id: "2",
Pk: "users#consistent_test",
Sk: "user#2",
},
},
expected: []User{
{
Id: "1",
Pk: "users#consistent_test",
Sk: "user#1",
},
{
Id: "2",
Pk: "users#consistent_test",
Sk: "user#2",
},
},
expectConsistentRead: true,
},
{
title: "query without consistent read (default eventual consistency)",
condition: "pk = :pk",
keys: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "users#eventual_test"},
},
opts: []dynago.QueryOptions{},
source: []User{
{
Id: "3",
Pk: "users#eventual_test",
Sk: "user#3",
},
},
expected: []User{
{
Id: "3",
Pk: "users#eventual_test",
Sk: "user#3",
},
},
expectConsistentRead: false,
},
{
title: "query with consistent read and other options",
condition: "pk = :pk",
keys: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "users#mixed_test"},
},
opts: []dynago.QueryOptions{
dynago.WithConsistentRead(),
dynago.WithLimit(1),
},
source: []User{
{
Id: "4",
Pk: "users#mixed_test",
Sk: "user#4",
},
{
Id: "5",
Pk: "users#mixed_test",
Sk: "user#5",
},
},
expected: []User{
{
Id: "4",
Pk: "users#mixed_test",
Sk: "user#4",
},
},
expectConsistentRead: true,
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

table := prepareTable(t)
condition, keys, opts, source, expected := tc.condition, tc.keys, tc.opts, tc.source, tc.expected
ctx := context.TODO()

// prepare the table, write test sample data
if len(source) > 0 {
items := make([]*dynago.TransactPutItemsInput, 0, len(source))
for _, item := range tc.source {
items = append(items, &dynago.TransactPutItemsInput{
PartitionKeyValue: dynago.StringValue(item.Pk),
SortKeyValue: dynago.StringValue(item.Sk),
Item: item,
})
}
err := table.TransactPutItems(ctx, items)
if err != nil {
t.Fatalf("unexpected error %s", err)
}
}

var out []User
_, err := table.Query(ctx, condition, keys, &out, opts...)
if err != nil {
t.Fatalf("unexpected error %s", err)
}

if !reflect.DeepEqual(expected, out) {
t.Errorf("expected query to return %v; got %v", expected, out)
}

// Note: We can't directly verify that ConsistentRead was set in the actual DynamoDB request
// because that would require mocking the AWS client. The test verifies that the function
// can be called without error and returns expected results.
})
}
}
Binary file added verify_api
Binary file not shown.