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

Docstore/awsdynamodb: Supporting the AWS V2 SDK #3519

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9bd155e
docstore: duplicating awsdynamodb as base for V2 implementation
Maldris Feb 10, 2025
9048729
docstore: updating codec tests for v2 decoder types
Maldris Feb 10, 2025
bcdacd3
docstore: rewriting aws dynamodb codec to use the types from v2 aws sdk
Maldris Feb 10, 2025
6220866
docstore: updating modules based on update progress
Maldris Feb 10, 2025
fcc9797
docstore: re-introducing codecTester
Maldris Feb 10, 2025
699d1ba
docstore: updating url opener parsing to use aws sdk v2 initializatio…
Maldris Feb 10, 2025
f159208
docstore: updating query planner and builder behaviors with aws sdk v…
Maldris Feb 11, 2025
8a26452
docstore: converting main dynamodb docstore logic to use aws sdk v2
Maldris Feb 11, 2025
698c7b1
docstore: updating example and dynamodb docstore benchmark and exampl…
Maldris Feb 11, 2025
7c55cb2
docstore: updating comment to point to v2 documentation of aws sessio…
Maldris Feb 11, 2025
8a91326
docstore: removing commented out v1 code used as reference
Maldris Feb 11, 2025
6945028
docstore: explicitly handle document encoding error (should never hap…
Maldris Feb 11, 2025
6026fb9
docstore: docstore/awsdynamodb's encodeDocKeyFields always returns ma…
Maldris Feb 11, 2025
f32c823
docstore: reviewed todos against v1 behavior and added explanatory co…
Maldris Feb 11, 2025
8785d1c
docstore: grouping encoder tests by type
Maldris Feb 11, 2025
9ca2692
docstore: adding tests for boolean false, multi-char and space contai…
Maldris Feb 11, 2025
778c153
docstore: centralizing attribute value ignore unexported definition
Maldris Feb 11, 2025
12aad2a
docstore: adding decoder tests
Maldris Feb 11, 2025
a79a608
docstore: fixing removed import label from merge
Maldris Feb 11, 2025
1887351
docstore: logging the test that fails for easier debug
Maldris Feb 11, 2025
3ef67d3
docstore: tests specify region, setup script does not, adding to make…
Maldris Feb 11, 2025
7c00fc8
docstore: fixing error as to asset the correct type
Maldris Feb 11, 2025
288d217
docstore: updating error code lookup using new error as behavior from…
Maldris Feb 11, 2025
ac4a182
docstore: fixing logic error in missing field key check
Maldris Feb 11, 2025
13954f2
docstore: fixing different map behavior in aws dynamodb v2 not matchi…
Maldris Feb 11, 2025
75e787b
docstore: updating error decoding to use correct type
Maldris Feb 11, 2025
e3fe7bf
docstore: clarifying errors with comments
Maldris Feb 11, 2025
339e9df
docstore/awsdynamodb moving V2 implementation to V2 folder
Maldris Mar 17, 2025
fa5c50b
fixing changed import path after moving driver folder
Maldris Mar 17, 2025
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
137 changes: 137 additions & 0 deletions docstore/awsdynamodb/v2/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"context"
"fmt"
"strconv"
"testing"

awsv2cfg "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
dyn2Types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

var benchmarkTableName = collectionName3

func BenchmarkPutVSTransact(b *testing.B) {
// This benchmark compares two ways to replace N items and retrieve their previous values.
// The first way makes N calls to PutItem with ReturnValues set to ALL_OLD.
// The second way calls BatchGetItem followed by TransactWriteItem.
//
// The results show that separate PutItems are faster for up to two items.
cfg, err := awsv2cfg.LoadDefaultConfig(context.Background())
if err != nil {
b.Fatal("Error initializing aws session for benchmark: ", err)
}
db := dynamodb.NewFromConfig(cfg)

for nItems := 1; nItems <= 5; nItems++ {
b.Run(fmt.Sprintf("%d-Items", nItems), func(b *testing.B) {
var items []map[string]dyn2Types.AttributeValue
for i := 0; i < nItems; i++ {
items = append(items, map[string]dyn2Types.AttributeValue{
"name": &dyn2Types.AttributeValueMemberS{Value: fmt.Sprintf("pt-vs-transact-%d", i)},
"x": &dyn2Types.AttributeValueMemberN{Value: strconv.Itoa(i)},
"rev": &dyn2Types.AttributeValueMemberN{Value: "1"},
})
}
for _, item := range items {
_, err := db.PutItem(context.Background(), &dynamodb.PutItemInput{
TableName: &benchmarkTableName,
Item: item,
})
if err != nil {
b.Fatal(err)
}
}
b.Run("PutItem", func(b *testing.B) {
for n := 0; n < b.N; n++ {
putItems(b, db, items)
}
})
b.Run("TransactWrite", func(b *testing.B) {
for n := 0; n < b.N; n++ {
batchGetTransactWrite(b, db, items)
}
})
})
}
}

func putItems(b *testing.B, db *dynamodb.Client, items []map[string]dyn2Types.AttributeValue) {
b.Helper()

for i, item := range items {
item["x"] = &dyn2Types.AttributeValueMemberN{Value: strconv.Itoa(i + 1)}
in := &dynamodb.PutItemInput{
TableName: &benchmarkTableName,
Item: item,
ReturnValues: dyn2Types.ReturnValueAllOld,
}
ce, err := expression.NewBuilder().
WithCondition(expression.Name("rev").Equal(expression.Value(1))).
Build()
if err != nil {
b.Fatal(err)
}
in.ExpressionAttributeNames = ce.Names()
in.ExpressionAttributeValues = ce.Values()
in.ConditionExpression = ce.Condition()
out, err := db.PutItem(context.Background(), in)
if err != nil {
b.Fatal(err)
}
if got, want := len(out.Attributes), 3; got != want {
b.Fatalf("got %d attributes, want %d", got, want)
}
}
}

func batchGetTransactWrite(b *testing.B, db *dynamodb.Client, items []map[string]dyn2Types.AttributeValue) {
b.Helper()

keys := make([]map[string]dyn2Types.AttributeValue, len(items))
tws := make([]dyn2Types.TransactWriteItem, len(items))
for i, item := range items {
keys[i] = map[string]dyn2Types.AttributeValue{"name": items[i]["name"]}
item["x"] = &dyn2Types.AttributeValueMemberN{Value: strconv.Itoa(i + 2)}
put := &dyn2Types.Put{TableName: &benchmarkTableName, Item: items[i]}
ce, err := expression.NewBuilder().
WithCondition(expression.Name("rev").Equal(expression.Value(1))).
Build()
if err != nil {
b.Fatal(err)
}
put.ExpressionAttributeNames = ce.Names()
put.ExpressionAttributeValues = ce.Values()
put.ConditionExpression = ce.Condition()
tws[i] = dyn2Types.TransactWriteItem{Put: put}
}
_, err := db.BatchGetItem(context.Background(), &dynamodb.BatchGetItemInput{
RequestItems: map[string]dyn2Types.KeysAndAttributes{
benchmarkTableName: {Keys: keys},
},
})
if err != nil {
b.Fatal(err)
}
_, err = db.TransactWriteItems(context.Background(), &dynamodb.TransactWriteItemsInput{TransactItems: tws})
if err != nil {
b.Fatal(err)
}
}
384 changes: 384 additions & 0 deletions docstore/awsdynamodb/v2/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"errors"
"fmt"
"reflect"
"strconv"
"time"

dyn2Types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"gocloud.dev/docstore/driver"
)

var nullValue = &dyn2Types.AttributeValueMemberNULL{Value: true}

type encoder struct {
av dyn2Types.AttributeValue
}

func (e *encoder) EncodeNil() { e.av = nullValue }
func (e *encoder) EncodeBool(x bool) { e.av = &dyn2Types.AttributeValueMemberBOOL{Value: x} }
func (e *encoder) EncodeInt(x int64) {
e.av = &dyn2Types.AttributeValueMemberN{Value: strconv.FormatInt(x, 10)}
}
func (e *encoder) EncodeUint(x uint64) {
e.av = &dyn2Types.AttributeValueMemberN{Value: strconv.FormatUint(x, 10)}
}
func (e *encoder) EncodeBytes(x []byte) { e.av = &dyn2Types.AttributeValueMemberB{Value: x} }
func (e *encoder) EncodeFloat(x float64) { e.av = encodeFloat(x) }

func (e *encoder) ListIndex(int) { panic("impossible") }
func (e *encoder) MapKey(string) { panic("impossible") }

func (e *encoder) EncodeString(x string) {
if len(x) == 0 {
e.av = nullValue
} else {
e.av = &dyn2Types.AttributeValueMemberS{Value: x}
}
}

func (e *encoder) EncodeComplex(x complex128) {
e.av = &dyn2Types.AttributeValueMemberL{Value: []dyn2Types.AttributeValue{encodeFloat(real(x)), encodeFloat(imag(x))}}
}

func (e *encoder) EncodeList(n int) driver.Encoder {
s := make([]dyn2Types.AttributeValue, n)
e.av = &dyn2Types.AttributeValueMemberL{Value: s}
return &listEncoder{s: s}
}

func (e *encoder) EncodeMap(n int) driver.Encoder {
m := make(map[string]dyn2Types.AttributeValue, n)
e.av = &dyn2Types.AttributeValueMemberM{Value: m}
return &mapEncoder{m: m}
}

var typeOfGoTime = reflect.TypeOf(time.Time{})

// EncodeSpecial encodes time.Time specially.
func (e *encoder) EncodeSpecial(v reflect.Value) (bool, error) {
switch v.Type() {
case typeOfGoTime:
ts := v.Interface().(time.Time).Format(time.RFC3339Nano)
e.EncodeString(ts)
default:
return false, nil
}
return true, nil
}

type listEncoder struct {
s []dyn2Types.AttributeValue
encoder
}

func (e *listEncoder) ListIndex(i int) { e.s[i] = e.av }

type mapEncoder struct {
m map[string]dyn2Types.AttributeValue
encoder
}

func (e *mapEncoder) MapKey(k string) { e.m[k] = e.av }

func encodeDoc(doc driver.Document) (dyn2Types.AttributeValue, error) {
var e encoder
if err := doc.Encode(&e); err != nil {
return nil, err
}
return e.av, nil
}

// Encode the key fields of the given document into a map AttributeValue.
// pkey and skey are the names of the partition key field and the sort key field.
// pkey must always be non-empty, but skey may be empty if the collection has no sort key.
func encodeDocKeyFields(doc driver.Document, pkey, skey string) (*dyn2Types.AttributeValueMemberM, error) {
m := map[string]dyn2Types.AttributeValue{}

set := func(fieldName string) error {
fieldVal, err := doc.GetField(fieldName)
if err != nil {
return err
}
attrVal, err := encodeValue(fieldVal)
if err != nil {
return err
}
m[fieldName] = attrVal
return nil
}

if err := set(pkey); err != nil {
return nil, err
}
if skey != "" {
if err := set(skey); err != nil {
return nil, err
}
}
return &dyn2Types.AttributeValueMemberM{Value: m}, nil
}

func encodeValue(v interface{}) (dyn2Types.AttributeValue, error) {
var e encoder
if err := driver.Encode(reflect.ValueOf(v), &e); err != nil {
return nil, err
}
return e.av, nil
}

func encodeFloat(f float64) dyn2Types.AttributeValue {
return &dyn2Types.AttributeValueMemberN{Value: strconv.FormatFloat(f, 'f', -1, 64)}
}

////////////////////////////////////////////////////////////////

func decodeDoc(item dyn2Types.AttributeValue, doc driver.Document) error {
return doc.Decode(decoder{av: item})
}

type decoder struct {
av dyn2Types.AttributeValue
}

func (d decoder) String() string {
if s, ok := d.av.(fmt.Stringer); ok {
return s.String()
}
return fmt.Sprint(d.av)
}

func (d decoder) AsBool() (bool, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberBOOL)
if !ok {
return false, false
}
return i.Value, true
}

func (d decoder) AsNull() bool {
i, ok := d.av.(*dyn2Types.AttributeValueMemberNULL)
if !ok {
return false
}
return i.Value
}

func (d decoder) AsString() (string, bool) {
// Empty string is represented by NULL.
_, ok := d.av.(*dyn2Types.AttributeValueMemberNULL)
if ok {
return "", true
}
i, ok := d.av.(*dyn2Types.AttributeValueMemberS)
if !ok {
return "", false
}
return i.Value, true
}

func (d decoder) AsInt() (int64, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberN)
if !ok {
return 0, false
}
v, err := strconv.ParseInt(i.Value, 10, 64)
if err != nil {
return 0, false
}
return v, true
}

func (d decoder) AsUint() (uint64, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberN)
if !ok {
return 0, false
}
v, err := strconv.ParseUint(i.Value, 10, 64)
if err != nil {
return 0, false
}
return v, true
}

func (d decoder) AsFloat() (float64, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberN)
if !ok {
return 0, false
}
v, err := strconv.ParseFloat(i.Value, 64)
if err != nil {
return 0, false
}
return v, true
}

func (d decoder) AsComplex() (complex128, bool) {
iface, ok := d.av.(*dyn2Types.AttributeValueMemberL)
if !ok {
return 0, false
}
if len(iface.Value) != 2 {
return 0, false
}
r, ok := decoder{iface.Value[0]}.AsFloat()
if !ok {
return 0, false
}
i, ok := decoder{iface.Value[1]}.AsFloat()
if !ok {
return 0, false
}
return complex(r, i), true
}

func (d decoder) AsBytes() ([]byte, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberB)
if !ok {
return nil, false
}
return i.Value, true
}

func (d decoder) ListLen() (int, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberL)
if !ok {
return 0, false
}
return len(i.Value), true
}

func (d decoder) DecodeList(f func(i int, vd driver.Decoder) bool) {
iface, ok := d.av.(*dyn2Types.AttributeValueMemberL)
if !ok {
// V1 behavior treated this as indistinct from an empty list,
// which this return replicates.
// Should this be explicitly handled in some way?
return
}
for i, el := range iface.Value {
if !f(i, decoder{el}) {
break
}
}
}

func (d decoder) MapLen() (int, bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberM)
if !ok {
return 0, false
}
return len(i.Value), true
}

func (d decoder) DecodeMap(f func(key string, vd driver.Decoder, exactMatch bool) bool) {
i, ok := d.av.(*dyn2Types.AttributeValueMemberM)
if !ok {
// V1 behavior treated this as indistinct from an empty map,
// which this return replicates.
// Should this be explicitly handled in some way?
return
}
for k, av := range i.Value {
if !f(k, decoder{av}, true) {
break
}
}
}

func (d decoder) AsInterface() (interface{}, error) {
return toGoValue(d.av)
}

func toGoValue(av dyn2Types.AttributeValue) (interface{}, error) {
switch v := av.(type) {
case *dyn2Types.AttributeValueMemberNULL:
return nil, nil
case *dyn2Types.AttributeValueMemberBOOL:
return v.Value, nil
case *dyn2Types.AttributeValueMemberN:
f, err := strconv.ParseFloat(v.Value, 64)
if err != nil {
return nil, err
}
i := int64(f)
if float64(i) == f {
return i, nil
}
u := uint64(f)
if float64(u) == f {
return u, nil
}
return f, nil

case *dyn2Types.AttributeValueMemberB:
return v.Value, nil
case *dyn2Types.AttributeValueMemberS:
return v.Value, nil

case *dyn2Types.AttributeValueMemberL:
s := make([]interface{}, len(v.Value))
for i, v := range v.Value {
x, err := toGoValue(v)
if err != nil {
return nil, err
}
s[i] = x
}
return s, nil

case *dyn2Types.AttributeValueMemberM:
m := make(map[string]interface{}, len(v.Value))
for k, v := range v.Value {
x, err := toGoValue(v)
if err != nil {
return nil, err
}
m[k] = x
}
return m, nil

default:
return nil, fmt.Errorf("awsdynamodb: AttributeValue %s not supported", av)
}
}

func (d decoder) AsSpecial(v reflect.Value) (bool, interface{}, error) {
unsupportedTypes := `unsupported type, the docstore driver for DynamoDB does
not decode DynamoDB set types, such as string set, number set and binary set`
switch d.av.(type) {
case *dyn2Types.AttributeValueMemberSS:
return true, nil, errors.New(unsupportedTypes)
case *dyn2Types.AttributeValueMemberNS:
return true, nil, errors.New(unsupportedTypes)
case *dyn2Types.AttributeValueMemberBS:
return true, nil, errors.New(unsupportedTypes)
}

switch v.Type() {
case typeOfGoTime:
i, ok := d.av.(*dyn2Types.AttributeValueMemberS)
if !ok {
return false, nil, errors.New("expected string field for time.Time")
}
t, err := time.Parse(time.RFC3339Nano, i.Value)
return true, t, err
}
return false, nil, nil
}
196 changes: 196 additions & 0 deletions docstore/awsdynamodb/v2/codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"reflect"
"testing"

dynattr "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
dyn2Types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"gocloud.dev/docstore/driver"
"gocloud.dev/docstore/drivertest"
)

var compareIgnoreAttributeUnexported = cmpopts.IgnoreUnexported(
dyn2Types.AttributeValueMemberB{},
dyn2Types.AttributeValueMemberBOOL{},
dyn2Types.AttributeValueMemberBS{},
dyn2Types.AttributeValueMemberL{},
dyn2Types.AttributeValueMemberM{},
dyn2Types.AttributeValueMemberN{},
dyn2Types.AttributeValueMemberNS{},
dyn2Types.AttributeValueMemberNULL{},
dyn2Types.AttributeValueMemberS{},
dyn2Types.AttributeValueMemberSS{},
)

func TestEncodeValue(t *testing.T) {
avn := func(s string) dyn2Types.AttributeValue { return &dyn2Types.AttributeValueMemberN{Value: s} }
avl := func(avs ...dyn2Types.AttributeValue) dyn2Types.AttributeValue {
return &dyn2Types.AttributeValueMemberL{Value: avs}
}

var seven int32 = 7
var nullptr *int

for _, test := range []struct {
in interface{}
want dyn2Types.AttributeValue
}{
// null
{nil, nullValue},
{nullptr, nullValue},
// number
{0, avn("0")},
{uint64(999), avn("999")},
{3.5, avn("3.5")},
{seven, avn("7")},
{&seven, avn("7")},
// string
{"", nullValue},
{"x", &dyn2Types.AttributeValueMemberS{Value: "x"}},
{"abc123", &dyn2Types.AttributeValueMemberS{Value: "abc123"}},
{"abc 123", &dyn2Types.AttributeValueMemberS{Value: "abc 123"}},
// bool
{true, &dyn2Types.AttributeValueMemberBOOL{Value: true}},
{false, &dyn2Types.AttributeValueMemberBOOL{Value: false}},
// list
{[]int(nil), nullValue},
{[]int{}, &dyn2Types.AttributeValueMemberL{Value: []dyn2Types.AttributeValue{}}},
{[]int{1, 2}, avl(avn("1"), avn("2"))},
{[...]int{1, 2}, avl(avn("1"), avn("2"))},
{[]interface{}{nil, false}, avl(nullValue, &dyn2Types.AttributeValueMemberBOOL{Value: false})},
// map
{map[string]int(nil), nullValue},
{map[string]int{}, &dyn2Types.AttributeValueMemberM{Value: map[string]dyn2Types.AttributeValue{}}},
{
map[string]int{"a": 1, "b": 2},
&dyn2Types.AttributeValueMemberM{Value: map[string]dyn2Types.AttributeValue{
"a": avn("1"),
"b": avn("2"),
}},
},
} {
var e encoder
if err := driver.Encode(reflect.ValueOf(test.in), &e); err != nil {
t.Fatal(err)
}
got := e.av
if !cmp.Equal(got, test.want, compareIgnoreAttributeUnexported) {
t.Errorf("%#v: got %#v, want %#v", test.in, got, test.want)
}
}
}

func TestDecodeValue(t *testing.T) {
avn := func(s string) dyn2Types.AttributeValue { return &dyn2Types.AttributeValueMemberN{Value: s} }
avl := func(vals ...dyn2Types.AttributeValue) dyn2Types.AttributeValue {
return &dyn2Types.AttributeValueMemberL{Value: vals}
}

for _, test := range []struct {
in dyn2Types.AttributeValue
want interface{}
}{
// null
// {nullValue, nil}, // cant reflect new, how best to test?
// bool
{&dyn2Types.AttributeValueMemberBOOL{Value: false}, false},
{&dyn2Types.AttributeValueMemberBOOL{Value: true}, true},
// string
{&dyn2Types.AttributeValueMemberS{Value: "x"}, "x"},
// int64
{avn("7"), int64(7)},
{avn("-7"), int64(-7)},
{avn("0"), int64(0)},
// uint64
{avn("7"), uint64(7)},
{avn("0"), uint64(0)},
// float64
{avn("7"), float64(7)},
{avn("0"), float64(0)},
{avn("3.1415"), float64(3.1415)},
// []byte
{&dyn2Types.AttributeValueMemberB{Value: []byte(`123`)}, []byte(`123`)},
// List
{avl(avn("12"), avn("37")), []int64{12, 37}},
{avl(avn("12"), avn("37")), []uint64{12, 37}},
{avl(avn("12.8"), avn("37.1")), []float64{12.8, 37.1}},
// Map
{
&dyn2Types.AttributeValueMemberM{Value: map[string]dyn2Types.AttributeValue{}},
map[string]int{},
},
{
&dyn2Types.AttributeValueMemberM{Value: map[string]dyn2Types.AttributeValue{"a": avn("1"), "b": avn("2")}},
map[string]int{"a": 1, "b": 2},
},
} {
var (
target = reflect.New(reflect.TypeOf(test.want))
)

dec := decoder{av: test.in}
if err := driver.Decode(target.Elem(), dec); err != nil {
t.Errorf(" error decoding value %#v, got error: %#v", test.in, err)
continue
}

if !cmp.Equal(target.Elem().Interface(), test.want, compareIgnoreAttributeUnexported) {
t.Errorf(" %#v: got %#v, want %#v", test.in, target.Elem().Interface(), test.want)
}
}
}

func TestDecodeErrorOnUnsupported(t *testing.T) {
for _, tc := range []struct {
in dyn2Types.AttributeValue
out interface{}
}{
{&dyn2Types.AttributeValueMemberSS{Value: []string{"foo", "bar"}}, []string{}},
{&dyn2Types.AttributeValueMemberNS{Value: []string{"1.1", "-2.2", "3.3"}}, []float64{}},
{&dyn2Types.AttributeValueMemberBS{Value: [][]byte{{4}, {5}, {6}}}, [][]byte{}},
} {
d := decoder{av: tc.in}
if err := driver.Decode(reflect.ValueOf(tc.out), &d); err == nil {
t.Error("got nil error, want unsupported error")
}
}
}

type codecTester struct{}

func (ct *codecTester) UnsupportedTypes() []drivertest.UnsupportedType {
return []drivertest.UnsupportedType{drivertest.BinarySet}
}

func (ct *codecTester) NativeEncode(obj interface{}) (interface{}, error) {
return dynattr.Marshal(obj)
}

func (ct *codecTester) NativeDecode(value, dest interface{}) error {
return dynattr.Unmarshal(value.(dyn2Types.AttributeValue), dest)
}

func (ct *codecTester) DocstoreEncode(obj interface{}) (interface{}, error) {
return encodeDoc(drivertest.MustDocument(obj))
}

func (ct *codecTester) DocstoreDecode(value, dest interface{}) error {
return decodeDoc(value.(dyn2Types.AttributeValue), drivertest.MustDocument(dest))
}
54 changes: 54 additions & 0 deletions docstore/awsdynamodb/v2/create_tables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Copyright 2019 The Go Cloud Development Kit Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Creates the DynamoDB tables needed for tests.
#
# If a table already exists, this script will fail. To re-create the table, run
# aws dynamodb delete-table --table-name ...
# and wait until the deletion completes.

# https://coderwall.com/p/fkfaqq/safer-bash-scripts-with-set-euxo-pipefail
# except we want to keep going if there is a failure.
set -uxo pipefail

# The docstore-test-1 table has a single partition key called "name".


aws dynamodb create-table \
--region us-east-2 \
--table-name docstore-test-1 \
--attribute-definitions AttributeName=name,AttributeType=S \
--key-schema AttributeName=name,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5


# The docstore-test-2 table has both a partition and a sort key, and two indexes.

aws dynamodb create-table \
--region us-east-2 \
--table-name docstore-test-2 \
--attribute-definitions \
AttributeName=Game,AttributeType=S \
AttributeName=Player,AttributeType=S \
AttributeName=Score,AttributeType=N \
AttributeName=Time,AttributeType=S \
--key-schema AttributeName=Game,KeyType=HASH AttributeName=Player,KeyType=RANGE \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
--local-secondary-indexes \
'IndexName=local,KeySchema=[{AttributeName=Game,KeyType=HASH},{AttributeName=Score,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
--global-secondary-indexes \
'IndexName=global,KeySchema=[{AttributeName=Player,KeyType=HASH},{AttributeName=Time,KeyType=RANGE}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}'


796 changes: 796 additions & 0 deletions docstore/awsdynamodb/v2/dynamo.go

Large diffs are not rendered by default.

230 changes: 230 additions & 0 deletions docstore/awsdynamodb/v2/dynamo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"context"
"errors"
"fmt"
"io"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
dyn "github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/smithy-go"
"gocloud.dev/docstore"
"gocloud.dev/docstore/driver"
"gocloud.dev/docstore/drivertest"
"gocloud.dev/gcerrors"
"gocloud.dev/internal/testing/setup"
)

// To create the tables and indexes needed for these tests, run create_tables.sh in
// this directory.
//
// The docstore-test-2 table is set up to work with queries on the drivertest.HighScore
// struct like so:
// table: "Game" partition key, "Player" sort key
// local index: "Game" partition key, "Score" sort key
// global index: "Player" partition key, "Time" sort key
// The conformance test queries should exercise all of these.
//
// The docstore-test-3 table is used for running benchmarks only. To eliminate
// the effect of dynamo auto-scaling, run:
// aws dynamodb update-table --table-name docstore-test-3 \
// --provisioned-throughput ReadCapacityUnits=1000,WriteCapacityUnits=1000
// Don't forget to change it back when done benchmarking.

const (
region = "us-east-2"
collectionName1 = "docstore-test-1"
collectionName2 = "docstore-test-2"
collectionName3 = "docstore-test-3" // for benchmark
)

type harness struct {
sess aws.Config
closer func()
}

func newHarness(ctx context.Context, t *testing.T) (drivertest.Harness, error) {
t.Helper()

sess, _, done, state := setup.NewAWSv2Config(ctx, t, region)
drivertest.MakeUniqueStringDeterministicForTesting(state)
return &harness{sess: sess, closer: done}, nil
}

func (*harness) BeforeDoTypes() []interface{} {
return []interface{}{
&dyn.BatchGetItemInput{}, &dyn.TransactWriteItemsInput{},
&dyn.PutItemInput{}, &dyn.DeleteItemInput{}, &dyn.UpdateItemInput{},
}
}

func (*harness) BeforeQueryTypes() []interface{} {
return []interface{}{&dyn.QueryInput{}, &dyn.ScanInput{}}
}

func (*harness) RevisionsEqual(rev1, rev2 interface{}) bool {
return rev1 == rev2
}

func (h *harness) Close() {
h.closer()
}

func (h *harness) MakeCollection(_ context.Context, kind drivertest.CollectionKind) (driver.Collection, error) {
switch kind {
case drivertest.SingleKey, drivertest.NoRev:
return newCollection(dyn.NewFromConfig(h.sess), collectionName1, drivertest.KeyField, "", &Options{
AllowScans: true,
ConsistentRead: true,
})
case drivertest.TwoKey:
// For query test we don't use strong consistency mode since some tests are
// running on global secondary index and it doesn't support ConsistentRead.
return newCollection(dyn.NewFromConfig(h.sess), collectionName2, "Game", "Player", &Options{
AllowScans: true,
RunQueryFallback: InMemorySortFallback(func() interface{} { return new(drivertest.HighScore) }),
})
case drivertest.AltRev:
return newCollection(dyn.NewFromConfig(h.sess), collectionName1, drivertest.KeyField, "",
&Options{
AllowScans: true,
RevisionField: drivertest.AlternateRevisionField,
ConsistentRead: true,
})
default:
panic("bad kind")
}
}

func collectHighScores(ctx context.Context, iter driver.DocumentIterator) ([]*drivertest.HighScore, error) {
var hs []*drivertest.HighScore
for {
var h drivertest.HighScore
doc := drivertest.MustDocument(&h)
err := iter.Next(ctx, doc)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
hs = append(hs, &h)
}
return hs, nil
}

type highScoreSliceIterator struct {
hs []*drivertest.HighScore
next int
}

func (it *highScoreSliceIterator) Next(ctx context.Context, doc driver.Document) error {
if it.next >= len(it.hs) {
return io.EOF
}
dest, ok := doc.Origin.(*drivertest.HighScore)
if !ok {
return fmt.Errorf("doc is %T, not HighScore", doc.Origin)
}
*dest = *it.hs[it.next]
it.next++
return nil
}

func (*highScoreSliceIterator) Stop() {}
func (*highScoreSliceIterator) As(interface{}) bool { return false }

type verifyAs struct{}

func (verifyAs) Name() string {
return "verify As"
}

func (verifyAs) CollectionCheck(coll *docstore.Collection) error {
var db *dyn.Client
if !coll.As(&db) {
return errors.New("Collection.As failed")
}
return nil
}

func (verifyAs) QueryCheck(it *docstore.DocumentIterator) error {
var so *dyn.ScanOutput
var qo *dyn.QueryOutput
if !it.As(&so) && !it.As(&qo) {
return errors.New("DocumentIterator.As failed")
}
return nil
}

func (v verifyAs) ErrorCheck(k *docstore.Collection, err error) error {
var e smithy.OperationError
if !k.ErrorAs(err, &e) {
return errors.New("Collection.ErrorAs failed")
}
return nil
}

func TestConformance(t *testing.T) {
// Note: when running -record repeatedly in a short time period, change the argument
// in the call below to generate unique transaction tokens.
drivertest.MakeUniqueStringDeterministicForTesting(1)
drivertest.RunConformanceTests(t, newHarness, &codecTester{}, []drivertest.AsTest{verifyAs{}})
}

func BenchmarkConformance(b *testing.B) {
cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))
if err != nil {
b.Fatal("Error loading AWS config for benchmark: ", err)
}
coll, err := newCollection(dyn.NewFromConfig(cfg), collectionName3, drivertest.KeyField, "", &Options{AllowScans: true})
if err != nil {
b.Fatal(err)
}
drivertest.RunBenchmarks(b, docstore.NewCollection(coll))
}

// awsdynamodb-specific tests.

func TestQueryErrors(t *testing.T) {
// Verify that bad queries return the right errors.
ctx := context.Background()
h, err := newHarness(ctx, t)
if err != nil {
t.Fatal(err)
}
defer h.Close()
dc, err := h.MakeCollection(ctx, drivertest.TwoKey)
if err != nil {
t.Fatal(err)
}
coll := docstore.NewCollection(dc)
defer coll.Close()

// Here we are comparing a key field with the wrong type. DynamoDB cares about this
// because even though it's a document store and hence schemaless, the key fields
// do have a schema (that is, they have known, fixed types).
iter := coll.Query().Where("Game", "=", 1).Get(ctx)
defer iter.Stop()
err = iter.Next(ctx, &h)
if c := gcerrors.Code(err); c != gcerrors.InvalidArgument {
t.Errorf("got %v (code %s, type %T), want InvalidArgument", err, c, err)
}
}
53 changes: 53 additions & 0 deletions docstore/awsdynamodb/v2/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb_test

import (
"context"
"log"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"gocloud.dev/docstore"
awsdynamodb "gocloud.dev/docstore/awsdynamodb/v2"
)

func ExampleOpenCollection() {
// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
log.Fatal(err)
}
coll, err := awsdynamodb.OpenCollection(
dynamodb.NewFromConfig(cfg), "docstore-test", "partitionKeyField", "", nil)
if err != nil {
log.Fatal(err)
}
defer coll.Close()
}

func Example_openCollectionFromURL() {
// PRAGMA: This example is used on gocloud.dev; PRAGMA comments adjust how it is shown and can be ignored.
// PRAGMA: On gocloud.dev, add a blank import: _ "gocloud.dev/docstore/awsdynamodb"
// PRAGMA: On gocloud.dev, hide lines until the next blank line.
ctx := context.Background()

// docstore.OpenCollection creates a *docstore.Collection from a URL.
coll, err := docstore.OpenCollection(ctx, "dynamodb://my-table?partition_key=name")
if err != nil {
log.Fatal(err)
}
defer coll.Close()
}
687 changes: 687 additions & 0 deletions docstore/awsdynamodb/v2/query.go

Large diffs are not rendered by default.

684 changes: 684 additions & 0 deletions docstore/awsdynamodb/v2/query_test.go

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1,144 changes: 1,144 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/ActionsWithCompositeID.replay

Large diffs are not rendered by default.

814 changes: 814 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/As/verify_As.replay

Large diffs are not rendered by default.

244 changes: 244 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/As/verify_As.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2,794 changes: 2,794 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/AtomicWrites.replay

Large diffs are not rendered by default.

2,739 changes: 2,739 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/AtomicWritesFail.replay

Large diffs are not rendered by default.

484 changes: 484 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/BeforeDo.replay

Large diffs are not rendered by default.

209 changes: 209 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/BeforeQuery.replay

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1,749 changes: 1,749 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Create.replay

Large diffs are not rendered by default.

1,914 changes: 1,914 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Data.replay

Large diffs are not rendered by default.

1,859 changes: 1,859 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Delete.replay

Large diffs are not rendered by default.

594 changes: 594 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/ExampleInDoc.replay

Large diffs are not rendered by default.

1,969 changes: 1,969 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Get.replay

Large diffs are not rendered by default.

2,684 changes: 2,684 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/GetQuery.replay

Large diffs are not rendered by default.

1,309 changes: 1,309 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/GetQueryKeyField.replay

Large diffs are not rendered by default.

3,619 changes: 3,619 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/MultipleActions.replay

Large diffs are not rendered by default.

319 changes: 319 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Proto.replay

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2,739 changes: 2,739 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Put.replay

Large diffs are not rendered by default.

2,354 changes: 2,354 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Replace.replay

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3,784 changes: 3,784 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestConformance/Update.replay

Large diffs are not rendered by default.

154 changes: 154 additions & 0 deletions docstore/awsdynamodb/v2/testdata/TestQueryErrors.replay

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions docstore/awsdynamodb/v2/urls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"context"
"errors"
"fmt"
"net/url"
"sync"

"github.com/aws/aws-sdk-go-v2/aws"
dyn "github.com/aws/aws-sdk-go-v2/service/dynamodb"
gcaws "gocloud.dev/aws"
"gocloud.dev/docstore"
)

func init() {
docstore.DefaultURLMux().RegisterCollection(Scheme, new(lazySessionOpener))
}

type lazySessionOpener struct {
init sync.Once
opener *URLOpener
err error
}

func (o *lazySessionOpener) OpenCollectionURL(ctx context.Context, u *url.URL) (*docstore.Collection, error) {
o.init.Do(func() {

o.opener = &URLOpener{}
})
if o.err != nil {
return nil, fmt.Errorf("open collection %s: %v", u, o.err)
}
return o.opener.OpenCollectionURL(ctx, u)
}

// Scheme is the URL scheme dynamodb registers its URLOpener under on
// docstore.DefaultMux.
const Scheme = "dynamodb"

// URLOpener opens dynamodb URLs like
// "dynamodb://mytable?partition_key=partkey&sort_key=sortkey".
//
// The URL Host is used as the table name. See
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
// for more details.
//
// The following query parameters are supported:
//
// - partition_key (required): the path to the partition key of a table or an index.
// - sort_key: the path to the sort key of a table or an index.
// - allow_scans: if "true", allow table scans to be used for queries
// - consistent_read: if "true", a strongly consistent read is used whenever possible.
//
// See https://godoc.org/gocloud.dev/aws#ConfigFromURLParams for supported query
// parameters for overriding the aws.Session from the URL.
type URLOpener struct {
}

// OpenCollectionURL opens the collection at the URL's path. See the package doc for more details.
func (o *URLOpener) OpenCollectionURL(_ context.Context, u *url.URL) (*docstore.Collection, error) {
db, tableName, partitionKey, sortKey, opts, err := o.processURL(u)
if err != nil {
return nil, err
}
return OpenCollection(db, tableName, partitionKey, sortKey, opts)
}

func (o *URLOpener) processURL(u *url.URL) (db *dyn.Client, tableName, partitionKey, sortKey string, opts *Options, err error) {
q := u.Query()

partitionKey = q.Get("partition_key")
if partitionKey == "" {
return nil, "", "", "", nil, fmt.Errorf("open collection %s: partition_key is required to open a table", u)
}
q.Del("partition_key")
sortKey = q.Get("sort_key")
q.Del("sort_key")
opts = &Options{
AllowScans: q.Get("allow_scans") == "true",
RevisionField: q.Get("revision_field"),
ConsistentRead: q.Get("consistent_read") == "true",
}
q.Del("allow_scans")
q.Del("revision_field")
q.Del("consistent_read")

tableName = u.Host
if tableName == "" {
return nil, "", "", "", nil, fmt.Errorf("open collection %s: URL's host cannot be empty (the table name)", u)
}
if u.Path != "" {
return nil, "", "", "", nil, fmt.Errorf("open collection %s: URL path must be empty, only the host is needed", u)
}

cfg, err := gcaws.V2ConfigFromURLParams(context.Background(), q)
if err != nil {
return nil, "", "", "", nil, fmt.Errorf("open collection %s: %v", u, err)
}
db, err = Dial(cfg)
if err != nil {
return nil, "", "", "", nil, fmt.Errorf("open collection %s: %v", u, err)
}
return db, tableName, partitionKey, sortKey, opts, nil
}

// Dial gets an AWS DynamoDB service client.
func Dial(p aws.Config) (*dyn.Client, error) {
if p.Credentials == nil {
return nil, errors.New("getting Dynamo service: no AWS session provided")
}
return dyn.NewFromConfig(p), nil
}
58 changes: 58 additions & 0 deletions docstore/awsdynamodb/v2/urls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2019 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package awsdynamodb

import (
"net/url"
"testing"
)

func TestProcessURL(t *testing.T) {
tests := []struct {
URL string
WantErr bool
}{
// OK.
{"dynamodb://docstore-test?partition_key=_kind", false},
// OK.
{"dynamodb://docstore-test?partition_key=_kind&sort_key=_id", false},
// OK, overriding region.
{"dynamodb://docstore-test?partition_key=_kind&region=" + region, false},
// OK, allow_scans.
{"dynamodb://docstore-test?partition_key=_kind&allow_scans=true" + region, false},
// Passing revision field.
{"dynamodb://docstore-test?partition_key=_kind&revision_field=123", false},
// Passing consistent read field.
{"dynamodb://docstore-test?partition_key=_kind&consistent_read=true", false},
// Unknown parameter.
{"dynamodb://docstore-test?partition_key=_kind&param=value", true},
// With path.
{"dynamodb://docstore-test/subcoll?partition_key=_kind", true},
// Missing partition_key.
{"dynamodb://docstore-test?sort_key=_id", true},
}

o := &URLOpener{}
for _, test := range tests {
u, err := url.Parse(test.URL)
if err != nil {
t.Fatal(err)
}
_, _, _, _, _, err = o.processURL(u)
if (err != nil) != test.WantErr {
t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
}
}
}
15 changes: 10 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -37,17 +37,20 @@ require (
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0
github.com/aws/aws-sdk-go v1.55.5
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/aws/aws-sdk-go-v2 v1.34.0
github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.16.0
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.64
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.6
github.com/aws/aws-sdk-go-v2/service/kms v1.35.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4
github.com/aws/aws-sdk-go-v2/service/sns v1.31.3
github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3
github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4
github.com/aws/smithy-go v1.20.3
github.com/aws/smithy-go v1.22.2
github.com/fsnotify/fsnotify v1.7.0
github.com/go-sql-driver/mysql v1.8.1
github.com/google/go-cmp v0.6.0
@@ -83,12 +86,14 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // 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.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
30 changes: 20 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -111,30 +111,40 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRB
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
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.34.0 h1:9iyL+cjifckRGEVpRKZP3eIxVlL06Qk1Tk13vreaVQU=
github.com/aws/aws-sdk-go-v2 v1.34.0/go.mod h1:JgstGg0JjWU1KpVJjD5H0y0yyAIpSdKEq556EI6yOOM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.16.0 h1:bSfq5lT2a58q5kbyqXGnUr2YX3sWtjRqm69eNcOWau0=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.16.0/go.mod h1:FBqEl9aG/k3FY7jHAq7CqznoDY4dp6DIm5ktxY4QkDw=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.64 h1:RbvNew9AKOcU7hsb7Bm70GWgKpN9QmGsV/CNjFnjG/I=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.64/go.mod h1:Ht5FhjqSJCW4K4IP7FvlySVt3G1M08dHXt1jHy6rIuQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
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/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/configsources v1.3.29 h1:Ej0Rf3GMv50Qh4G4852j2djtoDb7AzQ7MuQeFHa3D70=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.29/go.mod h1:oeNTC7PwJNoM5AznVr23wxhLnuJv0ZDe5v7w0wqIs9M=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.29 h1:6e8a71X+9GfghragVevC5bZqvATtc3mAMgxpSNbgzF0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.29/go.mod h1:c4jkZiQ+BWpNqq7VtrxjwISrLrt/VvPq3XiopkUIolI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
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/dynamodb v1.39.6 h1:OBoVhuZ7zXKziB4Kyd1lDUzysef2zWY8pC2Doc0zuiQ=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.6/go.mod h1:P4zDzUQq/lYgWGFzXNAKkyyMtlTqWvroS3IPQ18SnLw=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.16 h1:ELyiy1hrMQT/vfmv47Qn/xzgHULUrYk8GtLkAf07MD4=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.24.16/go.mod h1:DaigcaD8K9oqmNkr2eoe/ELSEsGx11zOhcmS0ac2Q6c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.10 h1:dx6ou28o859SdI4UkuH98Awkuwg4RdHawE5s6pYMQiA=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.10/go.mod h1:ilKRWYwq8gS8Wkltnph4MJUTInZefn1C1shAAZchlGg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
@@ -157,8 +167,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrA
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
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/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=