Skip to content
Open
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
3 changes: 3 additions & 0 deletions .changelog/45357.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_dynamodb_table: Add support for multi-attribute keys in global secondary indexes. Introduces hash_keys and range_keys to the gsi block and makes hash_key optional for backwards compatibility.
```
148 changes: 126 additions & 22 deletions internal/service/dynamodb/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,14 @@ func resourceTable() *schema.Resource {
Schema: map[string]*schema.Schema{
"hash_key": {
Type: schema.TypeString,
Required: true,
Optional: true,
},
"hash_keys": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Copy link

Choose a reason for hiding this comment

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

for my edification, what does computed mean in this context?

Elem: &schema.Schema{Type: schema.TypeString},
MaxItems: 4,
},
names.AttrName: {
Type: schema.TypeString,
Expand All @@ -212,6 +219,13 @@ func resourceTable() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"range_keys": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
MaxItems: 4,
},
"read_capacity": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -2099,6 +2113,17 @@ func updateDiffGSI(oldGsi, newGsi []any, billingMode awstypes.BillingMode) ([]aw
if err != nil {
return ops, err
}
// Remove empty hash_keys/range_keys to avoid false positive diffs
if hks, ok := oldAttributes["hash_keys"]; ok {
if set, ok := hks.(*schema.Set); ok && set.Len() == 0 {
delete(oldAttributes, "hash_keys")
Comment on lines +2118 to +2119
Copy link

Choose a reason for hiding this comment

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

should we instead throw error when a customer picks both options?

}
}
if rks, ok := oldAttributes["range_keys"]; ok {
if set, ok := rks.(*schema.Set); ok && set.Len() == 0 {
delete(oldAttributes, "range_keys")
}
}
newAttributes, err := stripCapacityAttributes(newMap)
if err != nil {
return ops, err
Expand All @@ -2115,6 +2140,17 @@ func updateDiffGSI(oldGsi, newGsi []any, billingMode awstypes.BillingMode) ([]aw
if err != nil {
return ops, err
}
// Remove empty hash_keys/range_keys to avoid false positive diffs
if hks, ok := newAttributes["hash_keys"]; ok {
if set, ok := hks.(*schema.Set); ok && set.Len() == 0 {
delete(newAttributes, "hash_keys")
}
}
if rks, ok := newAttributes["range_keys"]; ok {
if set, ok := rks.(*schema.Set); ok && set.Len() == 0 {
delete(newAttributes, "range_keys")
}
}
gsiNeedsRecreate := nonKeyAttributesChanged || !reflect.DeepEqual(oldAttributes, newAttributes) || warmThroughPutDecreased

// One step in most cases, an extra step in case of warmThroughputChanged without recreation necessity:
Expand Down Expand Up @@ -2613,15 +2649,29 @@ func flattenTableGlobalSecondaryIndex(gsi []awstypes.GlobalSecondaryIndexDescrip
gsi[names.AttrName] = aws.ToString(g.IndexName)
}

var hashKeys []string
var rangeKeys []string

for _, attribute := range g.KeySchema {
if attribute.KeyType == awstypes.KeyTypeHash {
gsi["hash_key"] = aws.ToString(attribute.AttributeName)
hashKeys = append(hashKeys, aws.ToString(attribute.AttributeName))
}

if attribute.KeyType == awstypes.KeyTypeRange {
gsi["range_key"] = aws.ToString(attribute.AttributeName)
rangeKeys = append(rangeKeys, aws.ToString(attribute.AttributeName))
}
}
// Set single values or lists based on count
if len(hashKeys) == 1 {
gsi["hash_key"] = hashKeys[0]
} else if len(hashKeys) > 1 {
gsi["hash_keys"] = hashKeys
}

if len(rangeKeys) == 1 {
gsi["range_key"] = rangeKeys[0]
} else if len(rangeKeys) > 1 {
gsi["range_keys"] = rangeKeys
Comment on lines +2664 to +2673
Copy link

Choose a reason for hiding this comment

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

should we internally always just start using hash_keys and range_keys and the customer facing api model can still contain the single range_key which internally just gets translated to a singleton list of range_keys

}

if g.Projection != nil {
gsi["projection_type"] = g.Projection.ProjectionType
Expand Down Expand Up @@ -2939,18 +2989,45 @@ func expandProjection(data map[string]any) *awstypes.Projection {
func expandKeySchema(data map[string]any) []awstypes.KeySchemaElement {
keySchema := []awstypes.KeySchemaElement{}

if v, ok := data["hash_key"]; ok && v != nil && v != "" {
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(v.(string)),
KeyType: awstypes.KeyTypeHash,
})
hKey, hKok := data["hash_key"]
hKeys, hKsok := data["hash_keys"].(*schema.Set)
rKey, rKok := data["range_key"]
rKeys, rKsok := data["range_keys"].(*schema.Set)

if hKok || hKsok {
if (hKey != nil && hKey != "") && (hKeys == nil || hKeys.Len() == 0) {
// use hash_key
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(hKey.(string)),
KeyType: awstypes.KeyTypeHash,
})
} else if hKeys != nil && hKeys.Len() > 0 {
//use hash_keys
for _, hKeyItem := range hKeys.List() {
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(hKeyItem.(string)),
KeyType: awstypes.KeyTypeHash,
})
}
}
}

if v, ok := data["range_key"]; ok && v != nil && v != "" {
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(v.(string)),
KeyType: awstypes.KeyTypeRange,
})
if rKok || rKsok {
if (rKey != nil && rKey != "") && (rKeys == nil || rKeys.Len() == 0) {
// use range_key
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(rKey.(string)),
KeyType: awstypes.KeyTypeRange,
})
} else if rKeys != nil && rKeys.Len() > 0 {
// use range_keys
for _, rKeyItem := range rKeys.List() {
keySchema = append(keySchema, awstypes.KeySchemaElement{
AttributeName: aws.String(rKeyItem.(string)),
KeyType: awstypes.KeyTypeRange,
})
}
}
}

return keySchema
Expand Down Expand Up @@ -3062,9 +3139,9 @@ func expandS3BucketSource(data map[string]any) *awstypes.S3BucketSource {
// validators

func validateTableAttributes(d *schema.ResourceDiff) error {
var errs []error
// Collect all indexed attributes
indexedAttributes := map[string]bool{}

if v, ok := d.GetOk("hash_key"); ok {
indexedAttributes[v.(string)] = true
}
Expand All @@ -3081,14 +3158,43 @@ func validateTableAttributes(d *schema.ResourceDiff) error {
}
if v, ok := d.GetOk("global_secondary_index"); ok {
indexes := v.(*schema.Set).List()

for _, idx := range indexes {
index := idx.(map[string]any)

hashKey := index["hash_key"].(string)
indexedAttributes[hashKey] = true

if rk, ok := index["range_key"].(string); ok && rk != "" {
indexedAttributes[rk] = true
hk, hkok := index["hash_key"].(string)
hks, hksok := index["hash_keys"].(*schema.Set)

if (hkok && hksok) && (hk != "" && hks.Len() > 0) {
errs = append(errs, fmt.Errorf("At most one can be set for hash_key (String type) or hash_keys (Set type) but both are set: %q, %v", hk, hks.List()))
} else {
// Check if hash_key is not empty then hash_keys must be empty and vice versa.
// then for whichever one is not empty check the indexed attributes
if hkok && hk != "" {
indexedAttributes[hk] = true
} else if hksok && hks.Len() > 0 {
for _, hk := range hks.List() {
if hkStr, ok := hk.(string); ok && hkStr != "" {
indexedAttributes[hkStr] = true
}
}
}
}
rk, rkok := index["range_key"].(string)
rks, rksok := index["range_keys"].(*schema.Set)

if (rkok && rksok) && (rk != "" && rks.Len() > 0) {
errs = append(errs, fmt.Errorf("At most one can be set for range_key (String type) or range_keys (Set type) but both are set: %q, %v", rk, rks.List()))
} else {
if rk, ok := index["range_key"].(string); ok && rk != "" {
indexedAttributes[rk] = true
} else if rks, ok := index["range_keys"].(*schema.Set); ok && rks.Len() > 0 {
for _, rk := range rks.List() {
if rkStr, ok := rk.(string); ok && rkStr != "" {
indexedAttributes[rkStr] = true
}
}
}
}
}
}
Expand All @@ -3107,8 +3213,6 @@ func validateTableAttributes(d *schema.ResourceDiff) error {
}
}

var errs []error

if len(unindexedAttributes) > 0 {
slices.Sort(unindexedAttributes)

Expand Down
Loading
Loading