diff --git a/dynamodb/attribute.go b/dynamodb/attribute.go index 1d4afd9d..f8c626e4 100755 --- a/dynamodb/attribute.go +++ b/dynamodb/attribute.go @@ -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" @@ -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 "" } @@ -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: @@ -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 { diff --git a/dynamodb/dynamodb.go b/dynamodb/dynamodb.go index ea71f02c..c66b3800 100755 --- a/dynamodb/dynamodb.go +++ b/dynamodb/dynamodb.go @@ -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 diff --git a/dynamodb/item.go b/dynamodb/item.go index cfb1e762..989116e3 100755 --- a/dynamodb/item.go +++ b/dynamodb/item.go @@ -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) @@ -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) diff --git a/dynamodb/item_test.go b/dynamodb/item_test.go index 6554703d..135beee8 100644 --- a/dynamodb/item_test.go +++ b/dynamodb/item_test.go @@ -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 { diff --git a/dynamodb/query_builder.go b/dynamodb/query_builder.go index 70118ecd..1b7da490 100644 --- a/dynamodb/query_builder.go +++ b/dynamodb/query_builder.go @@ -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 } diff --git a/dynamodb/query_builder_test.go b/dynamodb/query_builder_test.go index 61560c78..15d0409d 100755 --- a/dynamodb/query_builder_test.go +++ b/dynamodb/query_builder_test.go @@ -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}