Skip to content

Commit

Permalink
Merge pull request #339 from DramaFever/gh308_full_document_support
Browse files Browse the repository at this point in the history
Expanded support for map type in DynamoDB
  • Loading branch information
alimoeeny committed Feb 10, 2015
2 parents d22f039 + eb62881 commit c73835d
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 13 deletions.
23 changes: 22 additions & 1 deletion dynamodb/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
TYPE_STRING_SET = "SS"
TYPE_NUMBER_SET = "NS"
TYPE_BINARY_SET = "BS"
TYPE_MAP = "M"

COMPARISON_EQUAL = "EQ"
COMPARISON_NOT_EQUAL = "NE"
Expand Down Expand Up @@ -43,6 +44,7 @@ type Attribute struct {
Name string
Value string
SetValues []string
MapValues map[string]*Attribute
Exists string // exists on dynamodb? Values: "true", "false", or ""
}

Expand Down Expand Up @@ -140,6 +142,14 @@ func NewBinarySetAttribute(name string, values []string) *Attribute {
}
}

func NewMapAttribute(name string, values map[string]*Attribute) *Attribute {
return &Attribute{
Type: TYPE_MAP,
Name: name,
MapValues: values,
}
}

func (a *Attribute) SetType() bool {
switch a.Type {
case TYPE_BINARY_SET, TYPE_NUMBER_SET, TYPE_STRING_SET:
Expand All @@ -158,7 +168,18 @@ func (a *Attribute) SetExists(exists bool) *Attribute {
}

func (a Attribute) valueMsi() msi {
return msi{a.Type: map[bool]interface{}{true: a.SetValues, false: a.Value}[a.SetType()]}
switch {
case a.SetType():
return msi{a.Type: a.SetValues}
case a.Type == TYPE_MAP:
b := msi{}
for _, nestedAttr := range a.MapValues {
b[nestedAttr.Name] = nestedAttr.valueMsi()
}
return msi{a.Type: b}
default:
return msi{a.Type: a.Value}
}
}

func (k *PrimaryKey) HasRange() bool {
Expand Down
1 change: 1 addition & 0 deletions dynamodb/dynamodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (s *Server) queryServer(target string, query Query) ([]byte, error) {
numRetries := 0
for {
data := strings.NewReader(query.String())

hreq, err := http.NewRequest("POST", s.Region.DynamoDBEndpoint+"/", data)
if err != nil {
return nil, err
Expand Down
41 changes: 29 additions & 12 deletions dynamodb/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,50 +336,60 @@ func (t *Table) DeleteDocument(key *Key) error {
}

func (t *Table) AddAttributes(key *Key, attributes []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, nil, nil, "ADD")
return t.modifyAttributes(key, attributes, nil, nil, nil, "ADD")
}

func (t *Table) UpdateAttributes(key *Key, attributes []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, nil, nil, "PUT")
return t.modifyAttributes(key, attributes, nil, nil, nil, "PUT")
}

func (t *Table) DeleteAttributes(key *Key, attributes []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, nil, nil, "DELETE")
return t.modifyAttributes(key, attributes, nil, nil, nil, "DELETE")
}

func (t *Table) ConditionalAddAttributes(key *Key, attributes, expected []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, expected, nil, "ADD")
return t.modifyAttributes(key, attributes, expected, nil, nil, "ADD")
}

func (t *Table) ConditionalUpdateAttributes(key *Key, attributes, expected []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, expected, nil, "PUT")
return t.modifyAttributes(key, attributes, expected, nil, nil, "PUT")
}

func (t *Table) ConditionalDeleteAttributes(key *Key, attributes, expected []Attribute) (bool, error) {
return t.modifyAttributes(key, attributes, expected, nil, "DELETE")
return t.modifyAttributes(key, attributes, expected, nil, nil, "DELETE")
}

func (t *Table) ConditionExpressionAddAttributes(key *Key, attributes []Attribute, condition *Expression) (bool, error) {
return t.modifyAttributes(key, attributes, nil, condition, "ADD")
return t.modifyAttributes(key, attributes, nil, condition, nil, "ADD")
}

func (t *Table) ConditionExpressionUpdateAttributes(key *Key, attributes []Attribute, condition *Expression) (bool, error) {
return t.modifyAttributes(key, attributes, nil, condition, "PUT")
return t.modifyAttributes(key, attributes, nil, condition, nil, "PUT")
}

func (t *Table) ConditionExpressionDeleteAttributes(key *Key, attributes []Attribute, condition *Expression) (bool, error) {
return t.modifyAttributes(key, attributes, nil, condition, "DELETE")
return t.modifyAttributes(key, attributes, nil, condition, nil, "DELETE")
}

func (t *Table) modifyAttributes(key *Key, attributes, expected []Attribute, condition *Expression, action string) (bool, error) {
func (t *Table) UpdateExpressionUpdateAttributes(key *Key, condition, update *Expression) (bool, error) {
return t.modifyAttributes(key, nil, nil, condition, update, "")
}

if len(attributes) == 0 {
func (t *Table) modifyAttributes(key *Key, attributes, expected []Attribute, condition, update *Expression, action string) (bool, error) {

if len(attributes) == 0 && update == nil {
return false, errors.New("At least one attribute is required.")
}

q := NewQuery(t)
q.AddKey(key)
q.AddUpdates(attributes, action)

if len(attributes) > 0 {
q.AddUpdates(attributes, action)
}
if update != nil {
q.AddUpdateExpression(update)
}

if expected != nil {
q.AddExpected(expected)
Expand Down Expand Up @@ -462,6 +472,13 @@ func parseAttributes(s map[string]interface{}) map[string]*Attribute {
Name: key,
SetValues: arry,
}
} else if vals, ok := v[TYPE_MAP].(map[string]interface{}); ok {
m := parseAttributes(vals)
results[key] = &Attribute{
Type: TYPE_MAP,
Name: key,
MapValues: m,
}
}
} else {
log.Printf("type assertion to map[string] interface{} failed for : %s\n ", value)
Expand Down
142 changes: 142 additions & 0 deletions dynamodb/item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,148 @@ func (s *ItemSuite) TestUpdateItemWithSet(c *check.C) {
}
}

func (s *ItemSuite) TestQueryScanWithMap(c *check.C) {
attrs := []Attribute{
*NewMapAttribute("Attr1",
map[string]*Attribute{
"SubAttr1": NewStringAttribute("SubAttr1", "SubAttr1Val"),
"SubAttr2": NewNumericAttribute("SubAttr2", "2"),
},
),
}
var rk string
if s.WithRange {
rk = "1"
}
if ok, err := s.table.PutItem("NewHashKeyVal", rk, attrs); !ok {
c.Fatal(err)
}
pk := &Key{HashKey: "NewHashKeyVal", RangeKey: rk}

// Scan
if out, err := s.table.Scan(nil); err != nil {
c.Fatal(err)
} else {
if len(out) != 1 {
c.Fatal("Got no result from scan")
}
item := out[0]
if val, ok := item["Attr1"]; ok {
c.Check(val, check.DeepEquals, &attrs[0])
} else {
c.Error("Expected Attr1 to be found")
}
}

// Query
q := NewQuery(s.table)
q.AddKey(pk)
eq := NewStringAttributeComparison("TestHashKey", COMPARISON_EQUAL, pk.HashKey)
q.AddKeyConditions([]AttributeComparison{*eq})

if out, _, err := s.table.QueryTable(q); err != nil {
c.Fatal(err)
} else {
if len(out) != 1 {
c.Fatal("Got no result from query")
}
item := out[0]
if val, ok := item["Attr1"]; ok {
c.Check(val, check.DeepEquals, &attrs[0])
} else {
c.Fatal("Expected Attr1 to be found")
}
}

}

func (s *ItemSuite) TestUpdateItemWithMap(c *check.C) {
attrs := []Attribute{
*NewMapAttribute("Attr1",
map[string]*Attribute{
"SubAttr1": NewStringAttribute("SubAttr1", "SubAttr1Val"),
"SubAttr2": NewNumericAttribute("SubAttr2", "2"),
},
),
}
var rk string
if s.WithRange {
rk = "1"
}
if ok, err := s.table.PutItem("NewHashKeyVal", rk, attrs); !ok {
c.Fatal(err)
}

// Verify the PutItem operation
pk := &Key{HashKey: "NewHashKeyVal", RangeKey: rk}
if item, err := s.table.GetItem(pk); err != nil {
c.Error(err)
} else {
if val, ok := item["Attr1"]; ok {
c.Check(val, check.DeepEquals, &attrs[0])
} else {
c.Error("Expected Attr1 to be found")
}
}

// Update the map attribute via UpdateItem API
updateAttr := NewStringAttribute(":3", "SubAttr3Val")
update := &Expression{
Text: "SET #a.#3 = :3",
AttributeNames: map[string]string{
"#a": "Attr1",
"#3": "SubAttr3",
},
AttributeValues: []Attribute{*updateAttr},
}
expected := []Attribute{
*NewMapAttribute("Attr1",
map[string]*Attribute{
"SubAttr1": NewStringAttribute("SubAttr1", "SubAttr1Val"),
"SubAttr2": NewNumericAttribute("SubAttr2", "2"),
"SubAttr3": NewStringAttribute("SubAttr3", "SubAttr3Val"),
},
),
}
if ok, err := s.table.UpdateExpressionUpdateAttributes(pk, nil, update); !ok {
c.Fatal(err)
}

// Verify the map attribute field has been updated
if item, err := s.table.GetItem(pk); err != nil {
c.Fatal(err)
} else {
if val, ok := item["Attr1"]; ok {
c.Check(val, check.DeepEquals, &expected[0])
} else {
c.Fatal("Expected Attr1 to be found")
}
}

// Overwrite the map via UpdateItem API
newAttrs := []Attribute{
*NewMapAttribute("Attr1",
map[string]*Attribute{
"SubAttr3": NewStringAttribute("SubAttr3", "SubAttr3Val"),
},
),
}
if ok, err := s.table.UpdateAttributes(pk, newAttrs); !ok {
c.Error(err)
}

// Verify the map attribute has been overwritten
if item, err := s.table.GetItem(pk); err != nil {
c.Fatal(err)
} else {
if val, ok := item["Attr1"]; ok {
c.Check(val, check.DeepEquals, &newAttrs[0])
} else {
c.Fatal("Expected Attr1 to be found")
}
}
}

func (s *ItemSuite) TestPutGetDeleteDocument(c *check.C) {
k := &Key{HashKey: "NewHashKeyVal"}
if s.WithRange {
Expand Down
1 change: 1 addition & 0 deletions dynamodb/query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ func (q *UntypedQuery) AddQueryFilter(comparisons []AttributeComparison) {
func (q *UntypedQuery) AddLimit(limit int64) {
q.buffer["Limit"] = limit
}

func (q *UntypedQuery) AddSelect(value string) {
q.buffer["Select"] = value
}
Expand Down
50 changes: 50 additions & 0 deletions dynamodb/query_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,56 @@ func (s *QueryBuilderSuite) TestAddUpdates(c *check.C) {
c.Check(queryJson, check.DeepEquals, expectedJson)
}

func (s *QueryBuilderSuite) TestMapUpdates(c *check.C) {
primary := NewStringAttribute("domain", "")
key := PrimaryKey{primary, nil}
table := s.server.NewTable("sites", key)

q := NewQuery(table)
q.AddKey(&Key{HashKey: "test"})

subAttr1 := NewStringAttribute(":Updates1", "subval1")
subAttr2 := NewNumericAttribute(":Updates2", "2")
exp := &Expression{
Text: "SET #Updates0.#Updates1=:Updates1, #Updates0.#Updates2=:Updates2",
AttributeNames: map[string]string{
"#Updates0": "Map",
"#Updates1": "submap1",
"#Updates2": "submap2",
},
AttributeValues: []Attribute{*subAttr1, *subAttr2},
}
q.AddUpdateExpression(exp)
queryJson, err := simplejson.NewJson([]byte(q.String()))
if err != nil {
c.Fatal(err)
}
expectedJson, err := simplejson.NewJson([]byte(`
{
"UpdateExpression": "SET #Updates0.#Updates1=:Updates1, #Updates0.#Updates2=:Updates2",
"ExpressionAttributeNames": {
"#Updates0": "Map",
"#Updates1": "submap1",
"#Updates2": "submap2"
},
"ExpressionAttributeValues": {
":Updates1": {"S": "subval1"},
":Updates2": {"N": "2"}
},
"Key": {
"domain": {
"S": "test"
}
},
"TableName": "sites"
}
`))
if err != nil {
c.Fatal(err)
}
c.Check(queryJson, check.DeepEquals, expectedJson)
}

func (s *QueryBuilderSuite) TestAddKeyConditions(c *check.C) {
primary := NewStringAttribute("domain", "")
key := PrimaryKey{primary, nil}
Expand Down

0 comments on commit c73835d

Please sign in to comment.