diff --git a/.changelog/45357.txt b/.changelog/45357.txt new file mode 100644 index 000000000000..dea9df1cec6f --- /dev/null +++ b/.changelog/45357.txt @@ -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. +``` \ No newline at end of file diff --git a/internal/service/dynamodb/table.go b/internal/service/dynamodb/table.go index 542cdc0a6064..08e57a5d31a3 100644 --- a/internal/service/dynamodb/table.go +++ b/internal/service/dynamodb/table.go @@ -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, + Elem: &schema.Schema{Type: schema.TypeString}, + MaxItems: 4, }, names.AttrName: { Type: schema.TypeString, @@ -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, @@ -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") + } + } + 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 @@ -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: @@ -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 + } if g.Projection != nil { gsi["projection_type"] = g.Projection.ProjectionType @@ -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 @@ -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 } @@ -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 + } + } + } } } } @@ -3107,8 +3213,6 @@ func validateTableAttributes(d *schema.ResourceDiff) error { } } - var errs []error - if len(unindexedAttributes) > 0 { slices.Sort(unindexedAttributes) diff --git a/internal/service/dynamodb/table_test.go b/internal/service/dynamodb/table_test.go index 9c1c50609a07..efdc545996c4 100644 --- a/internal/service/dynamodb/table_test.go +++ b/internal/service/dynamodb/table_test.go @@ -825,8 +825,12 @@ func TestAccDynamoDBTable_extended(t *testing.T) { }) } -func TestAccDynamoDBTable_enablePITR(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiHashKey(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -838,32 +842,62 @@ func TestAccDynamoDBTable_enablePITR(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_initialState(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - testAccCheckInitialTableConf(resourceName), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccTableConfig_backup(rName), + Config: testAccTableConfig_addSecondaryGSI_multipleHashKeys(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "35"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "hash_key", "TestTableHashKey"), + resource.TestCheckResourceAttr(resourceName, "range_key", "TestTableRangeKey"), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption.#", "0"), + resource.TestCheckResourceAttr(resourceName, "attribute.#", "5"), + resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "1"), + resource.TestCheckResourceAttr(resourceName, "local_secondary_index.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableHashKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableRangeKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestLSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "ReplacementGSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + names.AttrName: "ReplacementTestTableGSI", + "hash_keys.#": "2", + "range_key": "ReplacementGSIRangeKey", + "write_capacity": "5", + "read_capacity": "5", + "projection_type": "INCLUDE", + "non_key_attributes.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "global_secondary_index.*.non_key_attributes.*", "TestNonKeyAttribute"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "local_secondary_index.*", map[string]string{ + names.AttrName: "TestTableLSI", + "range_key": "TestLSIRangeKey", + "projection_type": "ALL", + }), ), }, }, }) } -func TestAccDynamoDBTable_enablePITRWithCustomRecoveryPeriod(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiHashKey_transition(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -887,29 +921,59 @@ func TestAccDynamoDBTable_enablePITRWithCustomRecoveryPeriod(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccTableConfig_pitrWithCustomRecovery(rName, 10), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "10"), - ), - }, - { - Config: testAccTableConfig_pitrWithCustomRecovery(rName, 30), + Config: testAccTableConfig_addSecondaryGSI_multipleHashKeys_transition(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "30"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "hash_key", "TestTableHashKey"), + resource.TestCheckResourceAttr(resourceName, "range_key", "TestTableRangeKey"), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption.#", "0"), + resource.TestCheckResourceAttr(resourceName, "attribute.#", "5"), + resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "1"), + resource.TestCheckResourceAttr(resourceName, "local_secondary_index.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableHashKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableHashKey2", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableRangeKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestLSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + names.AttrName: "InitialTestTableGSI", + "range_key": "TestGSIRangeKey", + "write_capacity": "1", + "read_capacity": "1", + "projection_type": "KEYS_ONLY", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "local_secondary_index.*", map[string]string{ + names.AttrName: "TestTableLSI", + "range_key": "TestLSIRangeKey", + "projection_type": "ALL", + }), ), }, }, }) } -func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisioned(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiHashKeyMutliRangeKey_maxSet(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -921,34 +985,62 @@ func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisioned(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingPayPerRequest(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccTableConfig_billingProvisioned(rName), + Config: testAccTableConfig_addSecondaryGSI_multipleHashKeysMultipelRangeKeys_max(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "hash_key", "TestTableHashKey"), + resource.TestCheckResourceAttr(resourceName, "range_key", "TestTableRangeKey"), resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption.#", "0"), + resource.TestCheckResourceAttr(resourceName, "attribute.#", "8"), + resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "1"), + resource.TestCheckResourceAttr(resourceName, "local_secondary_index.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableHashKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableRangeKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestLSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "ReplacementGSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + names.AttrName: "ReplacementTestTableGSI", + "hash_keys.#": "4", + "range_keys.#": "4", + "write_capacity": "5", + "read_capacity": "5", + "projection_type": "INCLUDE", + "non_key_attributes.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "global_secondary_index.*.non_key_attributes.*", "TestNonKeyAttribute"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "local_secondary_index.*", map[string]string{ + names.AttrName: "TestTableLSI", + "range_key": "TestLSIRangeKey", + "projection_type": "ALL", + }), ), }, }, }) } -func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisionedIgnoreChanges(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiRangeKey(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -960,12 +1052,10 @@ func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisionedIgnoreChanges(t CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingPayPerRequest(rName), + Config: testAccTableConfig_initialState(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), + testAccCheckInitialTableConf(resourceName), ), }, { @@ -974,26 +1064,62 @@ func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisionedIgnoreChanges(t ImportStateVerify: true, }, { - Config: testAccTableConfig_billingProvisionedIgnoreChanges(rName), + Config: testAccTableConfig_addSecondaryGSI_multipleRangeKeys(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "hash_key", "TestTableHashKey"), + resource.TestCheckResourceAttr(resourceName, "range_key", "TestTableRangeKey"), resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "1"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "server_side_encryption.#", "0"), + resource.TestCheckResourceAttr(resourceName, "attribute.#", "5"), + resource.TestCheckResourceAttr(resourceName, "global_secondary_index.#", "1"), + resource.TestCheckResourceAttr(resourceName, "local_secondary_index.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableHashKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestTableRangeKey", + names.AttrType: "S", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "TestLSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "attribute.*", map[string]string{ + names.AttrName: "ReplacementGSIRangeKey", + names.AttrType: "N", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + names.AttrName: "ReplacementTestTableGSI", + "hash_key": "TestTableHashKey", + "range_keys.#": "2", + "write_capacity": "5", + "read_capacity": "5", + "projection_type": "INCLUDE", + "non_key_attributes.#": "1", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "global_secondary_index.*.non_key_attributes.*", "TestNonKeyAttribute"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "local_secondary_index.*", map[string]string{ + names.AttrName: "TestTableLSI", + "range_key": "TestLSIRangeKey", + "projection_type": "ALL", + }), ), }, }, }) } -func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequest(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiRangeKey_singleAndMultiSet(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { t.Skip("skipping long-running test in short mode") } - var conf awstypes.TableDescription - resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ @@ -1003,40 +1129,19 @@ func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequest(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingProvisioned(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: testAccTableConfig_billingPayPerRequest(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), - ), + Config: testAccTableConfig_addSecondaryGSI_multipleRangeKeys_singleAndMultiSet(rName), + ExpectError: regexache.MustCompile(`At most one can be set for range_key`), }, }, }) } -func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequestIgnoreChanges(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiHashKey_singleAndMultiSet(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { t.Skip("skipping long-running test in short mode") } - var conf awstypes.TableDescription - resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ @@ -1046,38 +1151,59 @@ func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequestIgnoreChanges(t CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingProvisioned(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + Config: testAccTableConfig_addSecondaryGSI_multipleHashKeys_singleAndMultiSet(rName), + ExpectError: regexache.MustCompile(`At most one can be set for hash_key`), }, + }, + }) +} + +func TestAccDynamoDBTable_extended_gsiMultiHashKey_tooMany(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingPayPerRequestIgnoreChanges(rName), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), - ), + Config: testAccTableConfig_addSecondaryGSI_multipleHashKeys_tooMany(rName), + ExpectError: regexache.MustCompile(`Too many list items`), }, }, }) } -func TestAccDynamoDBTable_BillingModeGSI_payPerRequestToProvisioned(t *testing.T) { +func TestAccDynamoDBTable_extended_gsiMultiRangeKey_tooMany(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { t.Skip("skipping long-running test in short mode") } + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_addSecondaryGSI_multipleRangeKeys_tooMany(rName), + ExpectError: regexache.MustCompile(`Too many list items`), + }, + }, + }) +} + +func TestAccDynamoDBTable_enablePITR(t *testing.T) { + ctx := acctest.Context(t) var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1089,37 +1215,32 @@ func TestAccDynamoDBTable_BillingModeGSI_payPerRequestToProvisioned(t *testing.T CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingPayPerRequestGSI(rName), + Config: testAccTableConfig_initialState(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), + testAccCheckInitialTableConf(resourceName), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"global_secondary_index"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, { - Config: testAccTableConfig_billingProvisionedGSI(rName), + Config: testAccTableConfig_backup(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "35"), ), }, }, }) } -func TestAccDynamoDBTable_BillingModeGSI_provisionedToPayPerRequest(t *testing.T) { +func TestAccDynamoDBTable_enablePITRWithCustomRecoveryPeriod(t *testing.T) { ctx := acctest.Context(t) - if testing.Short() { - t.Skip("skipping long-running test in short mode") - } - var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1131,10 +1252,10 @@ func TestAccDynamoDBTable_BillingModeGSI_provisionedToPayPerRequest(t *testing.T CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_billingProvisionedGSI(rName), + Config: testAccTableConfig_initialState(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + testAccCheckInitialTableConf(resourceName), ), }, { @@ -1143,22 +1264,29 @@ func TestAccDynamoDBTable_BillingModeGSI_provisionedToPayPerRequest(t *testing.T ImportStateVerify: true, }, { - Config: testAccTableConfig_billingPayPerRequestGSI(rName), + Config: testAccTableConfig_pitrWithCustomRecovery(rName, 10), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "10"), + ), + }, + { + Config: testAccTableConfig_pitrWithCustomRecovery(rName, 30), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.#", "1"), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "point_in_time_recovery.0.recovery_period_in_days", "30"), ), }, }, }) } -func TestAccDynamoDBTable_BillingMode_payPerRequestBasic(t *testing.T) { +func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisioned(t *testing.T) { ctx := acctest.Context(t) - if testing.Short() { - t.Skip("skipping long-running test in short mode") - } - var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1184,19 +1312,19 @@ func TestAccDynamoDBTable_BillingMode_payPerRequestBasic(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccTableConfig_billingPayPerRequestGSI(rName), + Config: testAccTableConfig_billingProvisioned(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), - resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), ), }, }, }) } -func TestAccDynamoDBTable_onDemandThroughput(t *testing.T) { +func TestAccDynamoDBTable_BillingMode_payPerRequestToProvisionedIgnoreChanges(t *testing.T) { ctx := acctest.Context(t) var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" @@ -1209,13 +1337,12 @@ func TestAccDynamoDBTable_onDemandThroughput(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_onDemandThroughput(rName, 5, 5), + Config: testAccTableConfig_billingPayPerRequest(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "5"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "5"), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), ), }, { @@ -1224,41 +1351,24 @@ func TestAccDynamoDBTable_onDemandThroughput(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccTableConfig_onDemandThroughput(rName, 10, 10), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "10"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "10"), - ), - }, - { - Config: testAccTableConfig_onDemandThroughput(rName, 1, 10), - Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "1"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "10"), - ), - }, - { - Config: testAccTableConfig_onDemandThroughput(rName, -1, 5), + Config: testAccTableConfig_billingProvisionedIgnoreChanges(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "0"), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "5"), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "1"), ), }, }, }) } -func TestAccDynamoDBTable_gsiOnDemandThroughput(t *testing.T) { +func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequest(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1270,14 +1380,12 @@ func TestAccDynamoDBTable_gsiOnDemandThroughput(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_gsiOnDemandThroughput(rName, 5, 5), + Config: testAccTableConfig_billingProvisioned(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ - "on_demand_throughput.0.max_read_request_units": "5", - "on_demand_throughput.0.max_write_request_units": "5", - }), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), ), }, { @@ -1286,23 +1394,24 @@ func TestAccDynamoDBTable_gsiOnDemandThroughput(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccTableConfig_gsiOnDemandThroughput(rName, 10, 10), + Config: testAccTableConfig_billingPayPerRequest(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), - resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ - "on_demand_throughput.0.max_read_request_units": "10", - "on_demand_throughput.0.max_write_request_units": "10", - }), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), ), }, }, }) } -func TestAccDynamoDBTable_streamSpecification(t *testing.T) { +func TestAccDynamoDBTable_BillingMode_provisionedToPayPerRequestIgnoreChanges(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1314,13 +1423,12 @@ func TestAccDynamoDBTable_streamSpecification(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_streamSpecification(rName, true, "KEYS_ONLY"), + Config: testAccTableConfig_billingProvisioned(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "5"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "5"), ), }, { @@ -1329,21 +1437,24 @@ func TestAccDynamoDBTable_streamSpecification(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccTableConfig_streamSpecification(rName, false, ""), + Config: testAccTableConfig_billingPayPerRequestIgnoreChanges(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), ), }, }, }) } -func TestAccDynamoDBTable_streamSpecificationDiffs(t *testing.T) { +func TestAccDynamoDBTable_BillingModeGSI_payPerRequestToProvisioned(t *testing.T) { ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var conf awstypes.TableDescription resourceName := "aws_dynamodb_table.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1355,43 +1466,309 @@ func TestAccDynamoDBTable_streamSpecificationDiffs(t *testing.T) { CheckDestroy: testAccCheckTableDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccTableConfig_streamSpecification(rName, true, "KEYS_ONLY"), + Config: testAccTableConfig_billingPayPerRequestGSI(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), ), }, { - Config: testAccTableConfig_streamSpecification(rName, true, "NEW_IMAGE"), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"global_secondary_index"}, + }, + { + Config: testAccTableConfig_billingProvisionedGSI(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "NEW_IMAGE"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), ), }, + }, + }) +} + +func TestAccDynamoDBTable_BillingModeGSI_provisionedToPayPerRequest(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ { - Config: testAccTableConfig_streamSpecification(rName, false, "NEW_IMAGE"), + Config: testAccTableConfig_billingProvisionedGSI(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "NEW_IMAGE"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModeProvisioned)), ), }, { - Config: testAccTableConfig_streamSpecification(rName, false, "KEYS_ONLY"), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTableConfig_billingPayPerRequestGSI(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckInitialTableExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), - resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), - acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), - resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + ), + }, + }, + }) +} + +func TestAccDynamoDBTable_BillingMode_payPerRequestBasic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_billingPayPerRequest(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTableConfig_billingPayPerRequestGSI(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "read_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "write_capacity", "0"), + ), + }, + }, + }) +} + +func TestAccDynamoDBTable_onDemandThroughput(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_onDemandThroughput(rName, 5, 5), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "5"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "5"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTableConfig_onDemandThroughput(rName, 10, 10), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "10"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "10"), + ), + }, + { + Config: testAccTableConfig_onDemandThroughput(rName, 1, 10), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "1"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "10"), + ), + }, + { + Config: testAccTableConfig_onDemandThroughput(rName, -1, 5), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_read_request_units", "0"), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.0.max_write_request_units", "5"), + ), + }, + }, + }) +} + +func TestAccDynamoDBTable_gsiOnDemandThroughput(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_gsiOnDemandThroughput(rName, 5, 5), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + "on_demand_throughput.0.max_read_request_units": "5", + "on_demand_throughput.0.max_write_request_units": "5", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTableConfig_gsiOnDemandThroughput(rName, 10, 10), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "billing_mode", string(awstypes.BillingModePayPerRequest)), + resource.TestCheckResourceAttr(resourceName, "on_demand_throughput.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_secondary_index.*", map[string]string{ + "on_demand_throughput.0.max_read_request_units": "10", + "on_demand_throughput.0.max_write_request_units": "10", + }), + ), + }, + }, + }) +} + +func TestAccDynamoDBTable_streamSpecification(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_streamSpecification(rName, true, "KEYS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTableConfig_streamSpecification(rName, false, ""), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + ), + }, + }, + }) +} + +func TestAccDynamoDBTable_streamSpecificationDiffs(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.TableDescription + resourceName := "aws_dynamodb_table.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.DynamoDBServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTableDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTableConfig_streamSpecification(rName, true, "KEYS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + ), + }, + { + Config: testAccTableConfig_streamSpecification(rName, true, "NEW_IMAGE"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "NEW_IMAGE"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + ), + }, + { + Config: testAccTableConfig_streamSpecification(rName, false, "NEW_IMAGE"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "NEW_IMAGE"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), + ), + }, + { + Config: testAccTableConfig_streamSpecification(rName, false, "KEYS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInitialTableExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "stream_enabled", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stream_view_type", "KEYS_ONLY"), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrStreamARN, "dynamodb", regexache.MustCompile(`table/`+rName+`/stream/`+streamLabelRegex)), + resource.TestMatchResourceAttr(resourceName, "stream_label", regexache.MustCompile(`^`+streamLabelRegex+`$`)), ), }, { @@ -6172,71 +6549,454 @@ resource "aws_dynamodb_table" "test" { `, rName) } -func testAccTableConfig_billingPayPerRequestIgnoreChanges(rName string) string { +func testAccTableConfig_billingPayPerRequestIgnoreChanges(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + billing_mode = "PAY_PER_REQUEST" + hash_key = "TestTableHashKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + lifecycle { + ignore_changes = [read_capacity, write_capacity] + } +} +`, rName) +} + +func testAccTableConfig_billingProvisioned(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + billing_mode = "PROVISIONED" + hash_key = "TestTableHashKey" + + read_capacity = 5 + write_capacity = 5 + + attribute { + name = "TestTableHashKey" + type = "S" + } +} +`, rName) +} + +func testAccTableConfig_billingProvisionedIgnoreChanges(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + billing_mode = "PROVISIONED" + hash_key = "TestTableHashKey" + + read_capacity = 5 + write_capacity = 5 + + attribute { + name = "TestTableHashKey" + type = "S" + } + + lifecycle { + ignore_changes = [read_capacity, write_capacity] + } +} +`, rName) +} + +func testAccTableConfig_billingPayPerRequestGSI(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + billing_mode = "PAY_PER_REQUEST" + hash_key = "TestTableHashKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableGSIKey" + type = "S" + } + + global_secondary_index { + name = "TestTableGSI" + hash_key = "TestTableGSIKey" + projection_type = "KEYS_ONLY" + } +} +`, rName) +} + +func testAccTableConfig_billingProvisionedGSI(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + billing_mode = "PROVISIONED" + hash_key = "TestTableHashKey" + name = %[1]q + read_capacity = 1 + write_capacity = 1 + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableGSIKey" + type = "S" + } + + global_secondary_index { + hash_key = "TestTableGSIKey" + name = "TestTableGSI" + projection_type = "KEYS_ONLY" + read_capacity = 1 + write_capacity = 1 + } +} +`, rName) +} + +func testAccTableConfig_initialState(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 1 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableRangeKey" + type = "S" + } + + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "TestGSIRangeKey" + type = "S" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "InitialTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "TestGSIRangeKey" + write_capacity = 1 + read_capacity = 1 + projection_type = "KEYS_ONLY" + } +} +`, rName) +} + +func testAccTableConfig_initialStateEncryptionAmazonCMK(rName string, enabled bool) string { + return fmt.Sprintf(` +data "aws_kms_alias" "dynamodb" { + name = "alias/aws/dynamodb" +} + +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 + enable_key_rotation = true +} + +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 1 + write_capacity = 1 + hash_key = "TestTableHashKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + server_side_encryption { + enabled = %[2]t + } +} +`, rName, enabled) +} + +func testAccTableConfig_initialStateEncryptionBYOK(rName string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 + enable_key_rotation = true +} + +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.test.arn + } +} +`, rName) +} + +func testAccTableConfig_addSecondaryGSI(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableRangeKey" + type = "S" + } + + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "ReplacementGSIRangeKey" + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] + } +} +`, rName) +} + +func testAccTableConfig_addSecondaryGSI_multipleHashKeys(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableHashKey2" + type = "S" + } + + attribute { + name = "TestTableRangeKey" + type = "S" + } + + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "ReplacementTestTableGSI" + hash_keys = ["TestTableHashKey", "TestTableHashKey2"] + range_key = "ReplacementGSIRangeKey" + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] + } +} +`, rName) +} + +func testAccTableConfig_addSecondaryGSI_multipleHashKeys_transition(rName string) string { + return fmt.Sprintf(` +resource "aws_dynamodb_table" "test" { + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" + + attribute { + name = "TestTableHashKey" + type = "S" + } + + attribute { + name = "TestTableHashKey2" + type = "S" + } + + attribute { + name = "TestTableRangeKey" + type = "S" + } + + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "TestGSIRangeKey" + type = "S" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "InitialTestTableGSI" + hash_keys = ["TestTableHashKey", "TestTableHashKey2"] + range_key = "TestGSIRangeKey" + write_capacity = 1 + read_capacity = 1 + projection_type = "KEYS_ONLY" + } +} +`, rName) +} + +func testAccTableConfig_addSecondaryGSI_multipleHashKeysMultipelRangeKeys_max(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = %[1]q - billing_mode = "PAY_PER_REQUEST" - hash_key = "TestTableHashKey" + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" attribute { name = "TestTableHashKey" type = "S" } - lifecycle { - ignore_changes = [read_capacity, write_capacity] + attribute { + name = "TestTableHashKey2" + type = "S" } -} -`, rName) -} -func testAccTableConfig_billingProvisioned(rName string) string { - return fmt.Sprintf(` -resource "aws_dynamodb_table" "test" { - name = %[1]q - billing_mode = "PROVISIONED" - hash_key = "TestTableHashKey" + attribute { + name = "TestTableHashKey3" + type = "S" + } - read_capacity = 5 - write_capacity = 5 + attribute { + name = "TestTableHashKey4" + type = "S" + } attribute { - name = "TestTableHashKey" + name = "TestTableRangeKey" type = "S" } -} -`, rName) -} -func testAccTableConfig_billingProvisionedIgnoreChanges(rName string) string { - return fmt.Sprintf(` -resource "aws_dynamodb_table" "test" { - name = %[1]q - billing_mode = "PROVISIONED" - hash_key = "TestTableHashKey" + attribute { + name = "TestLSIRangeKey" + type = "N" + } - read_capacity = 5 - write_capacity = 5 + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } attribute { - name = "TestTableHashKey" - type = "S" + name = "ReplacementGSIRangeKey2" + type = "N" } - lifecycle { - ignore_changes = [read_capacity, write_capacity] + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "ReplacementTestTableGSI" + hash_keys = ["TestTableHashKey", "TestTableHashKey2", "TestTableHashKey3", "TestTableHashKey4"] + range_keys = ["ReplacementGSIRangeKey", "TestTableRangeKey", "TestLSIRangeKey", "ReplacementGSIRangeKey2"] + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] } } `, rName) } -func testAccTableConfig_billingPayPerRequestGSI(rName string) string { +func testAccTableConfig_addSecondaryGSI_multipleRangeKeys(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - name = %[1]q - billing_mode = "PAY_PER_REQUEST" - hash_key = "TestTableHashKey" + name = %[1]q + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" attribute { name = "TestTableHashKey" @@ -6244,27 +7004,52 @@ resource "aws_dynamodb_table" "test" { } attribute { - name = "TestTableGSIKey" + name = "TestTableRangeKey" type = "S" } + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey2" + type = "N" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + global_secondary_index { - name = "TestTableGSI" - hash_key = "TestTableGSIKey" - projection_type = "KEYS_ONLY" + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + range_keys = ["ReplacementGSIRangeKey", "ReplacementGSIRangeKey2"] + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] } } `, rName) } -func testAccTableConfig_billingProvisionedGSI(rName string) string { +func testAccTableConfig_addSecondaryGSI_multipleRangeKeys_singleAndMultiSet(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { - billing_mode = "PROVISIONED" - hash_key = "TestTableHashKey" name = %[1]q - read_capacity = 1 - write_capacity = 1 + read_capacity = 2 + write_capacity = 2 + hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" attribute { name = "TestTableHashKey" @@ -6272,26 +7057,50 @@ resource "aws_dynamodb_table" "test" { } attribute { - name = "TestTableGSIKey" + name = "TestTableRangeKey" type = "S" } + attribute { + name = "TestLSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey2" + type = "N" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + global_secondary_index { - hash_key = "TestTableGSIKey" - name = "TestTableGSI" - projection_type = "KEYS_ONLY" - read_capacity = 1 - write_capacity = 1 + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + range_key = "ReplacementGSIRangeKey" + range_keys = ["ReplacementGSIRangeKey", "ReplacementGSIRangeKey2"] + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] } } `, rName) } -func testAccTableConfig_initialState(rName string) string { +func testAccTableConfig_addSecondaryGSI_multipleHashKeys_singleAndMultiSet(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { name = %[1]q - read_capacity = 1 + read_capacity = 2 write_capacity = 2 hash_key = "TestTableHashKey" range_key = "TestTableRangeKey" @@ -6312,8 +7121,13 @@ resource "aws_dynamodb_table" "test" { } attribute { - name = "TestGSIRangeKey" - type = "S" + name = "ReplacementGSIRangeKey" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey2" + type = "N" } local_secondary_index { @@ -6323,75 +7137,78 @@ resource "aws_dynamodb_table" "test" { } global_secondary_index { - name = "InitialTestTableGSI" - hash_key = "TestTableHashKey" - range_key = "TestGSIRangeKey" - write_capacity = 1 - read_capacity = 1 - projection_type = "KEYS_ONLY" + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + hash_keys = ["TestTableRangeKey", "TestTableHashKey"] + range_key = "ReplacementGSIRangeKey" + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] } } `, rName) } -func testAccTableConfig_initialStateEncryptionAmazonCMK(rName string, enabled bool) string { +func testAccTableConfig_addSecondaryGSI_multipleRangeKeys_tooMany(rName string) string { return fmt.Sprintf(` -data "aws_kms_alias" "dynamodb" { - name = "alias/aws/dynamodb" -} - -resource "aws_kms_key" "test" { - description = %[1]q - deletion_window_in_days = 7 - enable_key_rotation = true -} - resource "aws_dynamodb_table" "test" { name = %[1]q - read_capacity = 1 - write_capacity = 1 + read_capacity = 2 + write_capacity = 2 hash_key = "TestTableHashKey" + range_key = "TestTableRangeKey" attribute { name = "TestTableHashKey" type = "S" } - server_side_encryption { - enabled = %[2]t + attribute { + name = "TestTableRangeKey" + type = "S" } -} -`, rName, enabled) -} -func testAccTableConfig_initialStateEncryptionBYOK(rName string) string { - return fmt.Sprintf(` -resource "aws_kms_key" "test" { - description = %[1]q - deletion_window_in_days = 7 - enable_key_rotation = true -} + attribute { + name = "TestLSIRangeKey" + type = "N" + } -resource "aws_dynamodb_table" "test" { - name = %[1]q - read_capacity = 2 - write_capacity = 2 - hash_key = "TestTableHashKey" + attribute { + name = "ReplacementGSIRangeKey" + type = "N" + } attribute { - name = "TestTableHashKey" - type = "S" + name = "ReplacementGSIRangeKey2" + type = "N" } - server_side_encryption { - enabled = true - kms_key_arn = aws_kms_key.test.arn + attribute { + name = "ReplacementGSIRangeKey3" + type = "N" + } + + local_secondary_index { + name = "TestTableLSI" + range_key = "TestLSIRangeKey" + projection_type = "ALL" + } + + global_secondary_index { + name = "ReplacementTestTableGSI" + hash_key = "TestTableHashKey" + range_keys = ["ReplacementGSIRangeKey", "ReplacementGSIRangeKey2", "TestLSIRangeKey", "TestTableRangeKey", "ReplacementGSIRangeKey3"] + write_capacity = 5 + read_capacity = 5 + projection_type = "INCLUDE" + non_key_attributes = ["TestNonKeyAttribute"] } } `, rName) } -func testAccTableConfig_addSecondaryGSI(rName string) string { +func testAccTableConfig_addSecondaryGSI_multipleHashKeys_tooMany(rName string) string { return fmt.Sprintf(` resource "aws_dynamodb_table" "test" { name = %[1]q @@ -6420,6 +7237,16 @@ resource "aws_dynamodb_table" "test" { type = "N" } + attribute { + name = "ReplacementGSIRangeKey2" + type = "N" + } + + attribute { + name = "ReplacementGSIRangeKey3" + type = "N" + } + local_secondary_index { name = "TestTableLSI" range_key = "TestLSIRangeKey" @@ -6428,7 +7255,7 @@ resource "aws_dynamodb_table" "test" { global_secondary_index { name = "ReplacementTestTableGSI" - hash_key = "TestTableHashKey" + hash_keys = ["TestTableHashKey", "ReplacementGSIRangeKey2", "TestLSIRangeKey", "TestTableRangeKey", "ReplacementGSIRangeKey3"] range_key = "ReplacementGSIRangeKey" write_capacity = 5 read_capacity = 5 diff --git a/website/docs/r/dynamodb_table.html.markdown b/website/docs/r/dynamodb_table.html.markdown index 09377bf7e686..9db94726e849 100644 --- a/website/docs/r/dynamodb_table.html.markdown +++ b/website/docs/r/dynamodb_table.html.markdown @@ -75,6 +75,83 @@ resource "aws_dynamodb_table" "basic-dynamodb-table" { } ``` +### Basic Example containing Global Secondary Indexs using Multi-attribute keys pattern + +The following dynamodb table description models the table and GSIs shown in the [AWS SDK example documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html) + +```terraform +resource "aws_dynamodb_table" "basic-dynamodb-table" { + name = "TournamentMatches" + billing_mode = "PROVISIONED" + read_capacity = 20 + write_capacity = 20 + hash_key = "matchId" + + attribute { + name = "matchId" + type = "S" + } + + attribute { + name = "tournamentId" + type = "S" + } + + attribute { + name = "region" + type = "S" + } + + attribute { + name = "round" + type = "S" + } + + attribute { + name = "bracket" + type = "S" + } + + attribute { + name = "playerId" + type = N + } + + attribute { + name = "matchDate" + type = S + } + + ttl { + attribute_name = "TimeToExist" + enabled = true + } + + global_secondary_index { + name = "TournamentRegionIndex" + hash_keys = ["tournamentId", "region"] + range_keys = ["round", "bracket", "matchId"] + write_capacity = 10 + read_capacity = 10 + projection_type = "ALL" + } + + global_secondary_index { + name = "PlayerMatchHistoryIndex" + hash_key = "playerId" + range_keys = ["matchDate", "round"] + write_capacity = 10 + read_capacity = 10 + projection_type = "ALL" + } + + tags = { + Name = "dynamodb-table-1" + Environment = "production" + } +} +``` + ### Global Tables This resource implements support for [DynamoDB Global Tables V2 (version 2019.11.21)](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/globaltables.V2.html) via `replica` configuration blocks. For working with [DynamoDB Global Tables V1 (version 2017.11.29)](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/globaltables.V1.html), see the [`aws_dynamodb_global_table` resource](/docs/providers/aws/r/dynamodb_global_table.html). @@ -296,12 +373,16 @@ The following arguments are optional: ### `global_secondary_index` -* `hash_key` - (Required) Name of the hash key in the index; must be defined as an attribute in the resource. +* `hash_key` and `hash_keys` are `mutually exclusive`, but one is `required`. Refer to [AWS SDK Documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html) + * `hash_key` - (Optional) Name of the hash key in the index; must be defined as an attribute in the resource. + * `hash_keys` - (Optional) List of the hash keys in the index; each must be defined as an attribute in the resource; max of 4. * `name` - (Required) Name of the index. * `non_key_attributes` - (Optional) Only required with `INCLUDE` as a projection type; a list of attributes to project into the index. These do not need to be defined as attributes on the table. * `on_demand_throughput` - (Optional) Sets the maximum number of read and write units for the specified on-demand index. See below. * `projection_type` - (Required) One of `ALL`, `INCLUDE` or `KEYS_ONLY` where `ALL` projects every attribute into the index, `KEYS_ONLY` projects into the index only the table and index hash_key and sort_key attributes , `INCLUDE` projects into the index all of the attributes that are defined in `non_key_attributes` in addition to the attributes that that`KEYS_ONLY` project. -* `range_key` - (Optional) Name of the range key; must be defined +* `range_key` and `range_keys` are `mutually exclusive`, but are both `optional`. Refer to [AWS SDK Documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html) + * `range_key` - (Optional) Name of the range key; must be defined. + * `range_keys` - (Optional) List of the range keys in the index; each must be defined as an attribute in the resource; max of 4. * `read_capacity` - (Optional) Number of read units for this index. Must be set if billing_mode is set to PROVISIONED. * `warm_throughput` - (Optional) Sets the number of warm read and write units for this index. See below. * `write_capacity` - (Optional) Number of write units for this index. Must be set if billing_mode is set to PROVISIONED.