From c3edb79758e152a60370fcaa251fdbbdda9bcc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cie=C5=9Blak?= Date: Tue, 5 Nov 2024 11:49:55 +0100 Subject: [PATCH] chore: Detect changes in lists and sets (#3147) Test cases for changes in lists and sets. From the given time, I only went through essential cases that consisted of: - Ignoring the order of list items after creation. - Having the ability to update an item while ignoring the order. For the testing, I created a test resource because currently, we don't have anything to test more complex examples of certain resource behaviors (temporary providers we create for custom diff testing are not sufficient in this case). The resource is only added to the resource list whenever a special env is set (we could remove it entirely and leave some documentation in the resource file (and acc test file) on how to prepare for the tests). The imitation of external storage was done by creating a struct and its global instance (some of the things needed to be exported since external changes tested in acceptance tests needed to access those). Certain resource fields were researched to test different approaches, each is described in the implementation file. Also added an assert on lists/sets that is able to assert items in order independent manner (it was needed for the tests of the proposals). > Note: Only lists were tested, because there was no major issue with them in current resources. For the lists the following issues were addressed: #420, #753 ## Next pr - Apply (parameterized tests in object renaming test cases) https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/3130#discussion_r1802651147 --- pkg/acceptance/bettertestspoc/README.md | 1 + .../bettertestspoc/assert/commons.go | 83 ++ pkg/acceptance/helpers/database_client.go | 16 +- pkg/acceptance/helpers/schema_client.go | 16 +- pkg/provider/provider.go | 10 +- .../object_renaming_lists_and_sets.go | 709 ++++++++++ ...renaming_lists_and_sets_acceptance_test.go | 1217 +++++++++++++++++ 7 files changed, 2035 insertions(+), 17 deletions(-) create mode 100644 pkg/resources/object_renaming_lists_and_sets.go create mode 100644 pkg/resources/object_renaming_lists_and_sets_acceptance_test.go diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index f5aea01d27..a24c6abe6d 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -351,4 +351,5 @@ func (w *WarehouseDatasourceShowOutputAssert) IsEmpty() { 1. Lists of objects are partially generated, and only parameter name is generated in some functions (the type has to be added manually). 2. `testing` is a package name that makes Go think that we want to have unnamed parameter there, but we just didn't generate the type for that field in the function argument. - generate assertions checking that time is not empty - we often do not compare time fields by value, but check if they are set +- utilize `ContainsExactlyInAnyOrder` function in `pkg/acceptance/bettertestspoc/assert/commons.go` to create asserts on collections that are order independent - support generating provider config and use generated configs in `pkg/provider/provider_acceptance_test.go` diff --git a/pkg/acceptance/bettertestspoc/assert/commons.go b/pkg/acceptance/bettertestspoc/assert/commons.go index 59e1c86ffa..aeb44da985 100644 --- a/pkg/acceptance/bettertestspoc/assert/commons.go +++ b/pkg/acceptance/bettertestspoc/assert/commons.go @@ -3,8 +3,13 @@ package assert import ( "errors" "fmt" + "slices" + "strconv" + "strings" "testing" + "golang.org/x/exp/maps" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -101,3 +106,81 @@ func AssertThatObject(t *testing.T, objectAssert InPlaceAssertionVerifier) { t.Helper() objectAssert.VerifyAll(t) } + +func ContainsExactlyInAnyOrder(resourceKey string, attributePath string, expectedItems []map[string]string) resource.TestCheckFunc { + return func(state *terraform.State) error { + var actualItems []map[string]string + var resourceValue *terraform.ResourceState + + if value, ok := state.RootModule().Resources[resourceKey]; ok { + resourceValue = value + } else { + return fmt.Errorf("resource %s not found", resourceKey) + } + + // Allocate space for actualItems and assert length + for attrKey, attrValue := range resourceValue.Primary.Attributes { + if strings.HasPrefix(attrKey, attributePath) { + attr := strings.TrimPrefix(attrKey, attributePath+".") + + if attr == "#" { + attrValueLen, err := strconv.Atoi(attrValue) + if err != nil { + return fmt.Errorf("failed to convert length of the attribute %s: %w", attrKey, err) + } + if len(expectedItems) != attrValueLen { + return fmt.Errorf("expected to find %d items in %s, but found %d", len(expectedItems), attributePath, attrValueLen) + } + + actualItems = make([]map[string]string, attrValueLen) + for i := range actualItems { + actualItems[i] = make(map[string]string) + } + } + } + } + + // Gather all actual items + for attrKey, attrValue := range resourceValue.Primary.Attributes { + if strings.HasPrefix(attrKey, attributePath) { + attr := strings.TrimPrefix(attrKey, attributePath+".") + + if strings.HasSuffix(attr, "%") || strings.HasSuffix(attr, "#") { + continue + } + + attrParts := strings.SplitN(attr, ".", 2) + index, indexErr := strconv.Atoi(attrParts[0]) + isIndex := indexErr == nil + + if len(attrParts) > 1 && isIndex { + itemKey := attrParts[1] + actualItems[index][itemKey] = attrValue + } + } + } + + errs := make([]error, 0) + for _, actualItem := range actualItems { + found := false + if slices.ContainsFunc(expectedItems, func(expected map[string]string) bool { return maps.Equal(expected, actualItem) }) { + found = true + } + if !found { + errs = append(errs, fmt.Errorf("unexpected item found: %s", actualItem)) + } + } + + for _, expectedItem := range expectedItems { + found := false + if slices.ContainsFunc(actualItems, func(actual map[string]string) bool { return maps.Equal(actual, expectedItem) }) { + found = true + } + if !found { + errs = append(errs, fmt.Errorf("expected item to be found, but it wasn't: %s", expectedItem)) + } + } + + return errors.Join(errs...) + } +} diff --git a/pkg/acceptance/helpers/database_client.go b/pkg/acceptance/helpers/database_client.go index 640dbb896f..a0ea93ebab 100644 --- a/pkg/acceptance/helpers/database_client.go +++ b/pkg/acceptance/helpers/database_client.go @@ -48,14 +48,6 @@ func (c *DatabaseClient) CreateDatabaseWithOptions(t *testing.T, id sdk.AccountO return database, c.DropDatabaseFunc(t, id) } -func (c *DatabaseClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, opts *sdk.AlterDatabaseOptions) { - t.Helper() - ctx := context.Background() - - err := c.client().Alter(ctx, id, opts) - require.NoError(t, err) -} - func (c *DatabaseClient) DropDatabaseFunc(t *testing.T, id sdk.AccountObjectIdentifier) func() { t.Helper() return func() { require.NoError(t, c.DropDatabase(t, id)) } @@ -192,3 +184,11 @@ func (c *DatabaseClient) ShowAllReplicationDatabases(t *testing.T) ([]sdk.Replic return c.context.client.ReplicationFunctions.ShowReplicationDatabases(ctx, nil) } + +func (c *DatabaseClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, opts *sdk.AlterDatabaseOptions) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, opts) + require.NoError(t, err) +} diff --git a/pkg/acceptance/helpers/schema_client.go b/pkg/acceptance/helpers/schema_client.go index c20f7a58fb..5373bb1156 100644 --- a/pkg/acceptance/helpers/schema_client.go +++ b/pkg/acceptance/helpers/schema_client.go @@ -55,14 +55,6 @@ func (c *SchemaClient) CreateSchemaWithOpts(t *testing.T, id sdk.DatabaseObjectI return schema, c.DropSchemaFunc(t, id) } -func (c *SchemaClient) Alter(t *testing.T, id sdk.DatabaseObjectIdentifier, opts *sdk.AlterSchemaOptions) { - t.Helper() - ctx := context.Background() - - err := c.client().Alter(ctx, id, opts) - require.NoError(t, err) -} - func (c *SchemaClient) DropSchemaFunc(t *testing.T, id sdk.DatabaseObjectIdentifier) func() { t.Helper() ctx := context.Background() @@ -108,3 +100,11 @@ func (c *SchemaClient) ShowWithOptions(t *testing.T, opts *sdk.ShowSchemaOptions require.NoError(t, err) return schemas } + +func (c *SchemaClient) Alter(t *testing.T, id sdk.DatabaseObjectIdentifier, opts *sdk.AlterSchemaOptions) { + t.Helper() + ctx := context.Background() + + err := c.client().Alter(ctx, id, opts) + require.NoError(t, err) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 9369ca0451..922e8b3f72 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/datasources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider/docs" @@ -417,7 +419,7 @@ func Provider() *schema.Provider { } func getResources() map[string]*schema.Resource { - return map[string]*schema.Resource{ + resourceList := map[string]*schema.Resource{ "snowflake_account": resources.Account(), "snowflake_account_authentication_policy_attachment": resources.AccountAuthenticationPolicyAttachment(), "snowflake_account_role": resources.AccountRole(), @@ -504,6 +506,12 @@ func getResources() map[string]*schema.Resource { "snowflake_view": resources.View(), "snowflake_warehouse": resources.Warehouse(), } + + if os.Getenv(string(testenvs.EnableObjectRenamingTest)) != "" { + resourceList["snowflake_object_renaming"] = resources.ObjectRenamingListsAndSets() + } + + return resourceList } func getDataSources() map[string]*schema.Resource { diff --git a/pkg/resources/object_renaming_lists_and_sets.go b/pkg/resources/object_renaming_lists_and_sets.go new file mode 100644 index 0000000000..7888018ce6 --- /dev/null +++ b/pkg/resources/object_renaming_lists_and_sets.go @@ -0,0 +1,709 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "golang.org/x/exp/slices" +) + +type ObjectRenamingDatabaseListItem struct { + String string + Int int +} + +func mapObjectRenamingDatabaseListItemFromValue(items []cty.Value) []ObjectRenamingDatabaseListItem { + return collections.Map(items, func(item cty.Value) ObjectRenamingDatabaseListItem { + intValue, _ := item.AsValueMap()["int"].AsBigFloat().Int64() + return ObjectRenamingDatabaseListItem{ + String: item.AsValueMap()["string"].AsString(), + Int: int(intValue), + } + }) +} + +func objectRenamingDatabaseListFromSchema(items []any) []ObjectRenamingDatabaseListItem { + return collections.Map(items, func(item any) ObjectRenamingDatabaseListItem { + return ObjectRenamingDatabaseListItem{ + String: item.(map[string]any)["string"].(string), + Int: item.(map[string]any)["int"].(int), + } + }) +} + +type objectRenamingDatabaseOrderedListItem struct { + Name string + Order string +} + +func mapObjectRenamingDatabaseOrderedListItemFromValue(items []cty.Value) []objectRenamingDatabaseOrderedListItem { + return collections.Map(items, func(item cty.Value) objectRenamingDatabaseOrderedListItem { + var order string + if orderValue, ok := item.AsValueMap()["order"]; ok && !orderValue.IsNull() { + order = orderValue.AsString() + } + var name string + if nameValue, ok := item.AsValueMap()["name"]; ok && !nameValue.IsNull() { + name = nameValue.AsString() + } + return objectRenamingDatabaseOrderedListItem{ + Name: name, + Order: order, + } + }) +} + +func objectRenamingDatabaseOrderedListFromSchema(list []any) []objectRenamingDatabaseOrderedListItem { + objectRenamingDatabaseOrderedListItems := make([]objectRenamingDatabaseOrderedListItem, len(list)) + for index, item := range list { + var name string + if nameValue, ok := item.(map[string]any)["name"]; ok { + name = nameValue.(string) + } + objectRenamingDatabaseOrderedListItems[index] = objectRenamingDatabaseOrderedListItem{ + Name: name, + Order: strconv.Itoa(index), + } + } + return objectRenamingDatabaseOrderedListItems +} + +type ObjectRenamingDatabaseManuallyOrderedListItem struct { + Name string + Type string +} + +func objectRenamingDatabaseManuallyOrderedListFromSchema(list []any) []ObjectRenamingDatabaseManuallyOrderedListItem { + objectRenamingDatabaseOrderedListItems := make([]ObjectRenamingDatabaseManuallyOrderedListItem, len(list)) + slices.SortFunc(list, func(a, b any) int { + return a.(map[string]any)["order"].(int) - b.(map[string]any)["order"].(int) + }) + for index, item := range list { + objectRenamingDatabaseOrderedListItems[index] = ObjectRenamingDatabaseManuallyOrderedListItem{ + Name: item.(map[string]any)["name"].(string), + Type: item.(map[string]any)["type"].(string), + } + } + return objectRenamingDatabaseOrderedListItems +} + +type objectRenamingDatabase struct { + List []ObjectRenamingDatabaseListItem + OrderedList []objectRenamingDatabaseOrderedListItem + ManuallyOrderedList []ObjectRenamingDatabaseManuallyOrderedListItem + ChangeLog ObjectRenamingDatabaseChangelog +} + +type ObjectRenamingDatabaseChangelogChange struct { + Before map[string]any + After map[string]any +} + +// ObjectRenamingDatabaseChangelog is used for testing purposes to track actions taken in the Update method like Add/Remove/Change. +// It's only supported the manually_ordered_list option. +type ObjectRenamingDatabaseChangelog struct { + Added []map[string]any + Removed []map[string]any + Changed []ObjectRenamingDatabaseChangelogChange +} + +var ObjectRenamingDatabaseInstance = &objectRenamingDatabase{ + List: make([]ObjectRenamingDatabaseListItem, 0), + OrderedList: make([]objectRenamingDatabaseOrderedListItem, 0), + ManuallyOrderedList: make([]ObjectRenamingDatabaseManuallyOrderedListItem, 0), + ChangeLog: ObjectRenamingDatabaseChangelog{}, +} + +var objectRenamingListsAndSetsSchema = map[string]*schema.Schema{ + // The list field was tested to be used in places where the order of the items should be ignored. + // It was ignored by comparing hashes of the items to see if any changes were made on the items themselves + // (if the hashes before and after were the same, we know that nothing was changed, only the order). + // Also, it doesn't fully support repeating items. This is because they have the same hash and to fully support it, + // hash counting could be added (counting if the same hash occurs in state and config the number of times, otherwise cause update). + // Modifications of the items will still cause remove/add behavior. + "list": { + Optional: true, + Type: schema.TypeList, + DiffSuppressFunc: ignoreListOrderAfterFirstApply("list"), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "string": { + Type: schema.TypeString, + Optional: true, + }, + "int": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + // The manually_ordered_list focuses on providing both aspects: + // - Immunity to item reordering after create. + // - Handling updates for changed items instead of removing the old item and adding a new one. + // It does it by providing the required order field that represents what should be the actual order of items + // on the Snowflake side. The order is ignored on the DiffSuppressFunc level, and the item update (renaming the item) + // is handled in the resource update function. This proposal is supposed to test the behavior of Snowflake columns needed for table refactor for v1. + // Here's the full list of what should be possible with this approach: + // Supported actions: + // - Drop item (any position). + // - Add item (at the end). + // - Rename item / Change item type (any position; compatible type change). + // - Reorder items. + // Unsupported actions: + // - Add item (in the middle). + // - Change item type (incompatible change). + // - External changes (with an option to set either ForceNew or error behavior). + // Assumptions: + // - The list "returned from Snowflake side" is ordered (or identifiable). + // - Order field is treated as an identifier that cannot be changed for the lifetime of a given item. + // - Items contain fields that are able to uniquely identify a given item (in this case, we have name + type). + "manually_ordered_list": { + Optional: true, + Type: schema.TypeList, + DiffSuppressFunc: ignoreOrderAfterFirstApplyWithManuallyOrderedList("manually_ordered_list"), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "order": { + Type: schema.TypeInt, + Required: true, + // Improvement: + // Cause ForceNew behavior whenever any of the items change their order to different value than previously. + // It's not trivial as it cannot be achieved by putting ForceNew modifier in the schema (with the current implementation of Update/Read/SuppressDiff). + // It also cannot be achieved by creating custom diff. It seems the custom diff is seeing the changes + // too late to call ForceNew and for Terraform to show it during the plan or apply it during the apply. + // Currently, the only good way to prevent such changes is to describe them clearly in the documentation. + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: sdkValidation(func(value string) (string, error) { + if slices.Contains([]string{"INT", "NUMBER", "STRING", "TEXT"}, value) { + return value, nil + } + return "", fmt.Errorf("invalid type: %s", value) + }), + }, + }, + }, + }, + // The invalid_operation_handler is a switch that we wanted to implement for the approach with manually_ordered_list. + // The idea behind it was to switch between different error handling behaviors for invalid operations we discussed + // (see unsupported actions in the manually_ordered_list description). For now, we wanted to have two options: + // error out or force re-creation (force new). Currently, the proposal uses only errors because during FORCE_NEW + // testing it seemed like it's impossible to for resource re-creation without adding more logic into custom diff + // that would be able to calculate it on plan time, not apply time. + "invalid_operation_handler": { + Type: schema.TypeString, + Optional: true, + Default: "ERROR", + ValidateDiagFunc: sdkValidation(func(value string) (string, error) { + if slices.Contains([]string{"ERROR", "FORCE_NEW"}, value) { + return value, nil + } + return "", fmt.Errorf("invalid invalid operation handler: %s", value) + }), + }, + // The invalid_operation field was an attempt to collect error messages gathered in the Update and Read methods + // and use it in custom diff that would try to re-create the resource on non-empty invalid_operation field. + // The FORCE_NEW handler applied this way seemed to have no impact on the resource behavior because the information was + // transferred to the field too late. As mentioned, it happened during Update and Read when the `terraform apply` is + // already running. During the run, it's not valid to apply the force new, because it has to be known before the apply, so + // Terraform would be able to show it during the plan. Because of that, we know that the logic inside custom diff has to + // do much more guessing on its own, so we would be able to know those invalid operations during the plan time. + // Only then we would be able to have invalid_operation_handler and FORCE_NEW as a valid option (and this field would be + // most likely useless and could be removed). + "invalid_operation": { + Type: schema.TypeString, + Computed: true, + }, + // The ordered_list field was an attempt of making manual work done in manually_ordered_list automatic by making the order field computed. + // It didn't work because in DiffSuppressFunc it's hard to get the computed value in the "after" state to compare against. + // Possibly (but with very low probability), the solution could work by introducing a Computed + Optional list that would be managed by a custom diff function. + // Due to increased complexity, it was left as is and more research was dedicated to manually_ordered_list. + "ordered_list": { + Optional: true, + Type: schema.TypeList, + DiffSuppressFunc: ignoreOrderAfterFirstApplyWithOrderedList("ordered_list"), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + }, + "order": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, +} + +func ObjectRenamingListsAndSets() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateObjectRenamingListsAndSets, + UpdateContext: UpdateObjectRenamingListsAndSets, + ReadContext: ReadObjectRenamingListsAndSets(true), + DeleteContext: DeleteObjectRenamingListsAndSets, + + Schema: objectRenamingListsAndSetsSchema, + } +} + +func CreateObjectRenamingListsAndSets(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ObjectRenamingDatabaseInstance.List = objectRenamingDatabaseListFromSchema(d.Get("list").([]any)) + ObjectRenamingDatabaseInstance.OrderedList = objectRenamingDatabaseOrderedListFromSchema(d.Get("ordered_list").([]any)) + ObjectRenamingDatabaseInstance.ManuallyOrderedList = objectRenamingDatabaseManuallyOrderedListFromSchema(d.Get("manually_ordered_list").([]any)) + + d.SetId("identifier") + + return ReadObjectRenamingListsAndSets(false)(ctx, d, meta) +} + +func UpdateObjectRenamingListsAndSets(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if d.HasChange("list") { + // It wasn't working with d.getChange(). It was returning null elements in one of the test case's steps. + oldList := d.GetRawState().AsValueMap()["list"].AsValueSlice() + newList := d.GetRawConfig().AsValueMap()["list"].AsValueSlice() + + oldListMapped := mapObjectRenamingDatabaseListItemFromValue(oldList) + newListMapped := mapObjectRenamingDatabaseListItemFromValue(newList) + + addedItems, removedItems := ListDiff(oldListMapped, newListMapped) + + for _, removedItem := range removedItems { + ObjectRenamingDatabaseInstance.List = slices.DeleteFunc(ObjectRenamingDatabaseInstance.List, func(item ObjectRenamingDatabaseListItem) bool { + shouldRemove := item == removedItem + if shouldRemove { + ObjectRenamingDatabaseInstance.ChangeLog.Removed = append(ObjectRenamingDatabaseInstance.ChangeLog.Removed, map[string]any{ + "int": item.Int, + "string": item.String, + }) + } + return shouldRemove + }) + } + + for _, addedItem := range addedItems { + ObjectRenamingDatabaseInstance.ChangeLog.Added = append(ObjectRenamingDatabaseInstance.ChangeLog.Added, map[string]any{ + "int": addedItem.Int, + "string": addedItem.String, + }) + } + + ObjectRenamingDatabaseInstance.List = append(ObjectRenamingDatabaseInstance.List, addedItems...) + } + + if d.HasChange("ordered_list") { + oldOrderedList := d.GetRawState().AsValueMap()["ordered_list"].AsValueSlice() + newOrderedList := d.GetRawConfig().AsValueMap()["ordered_list"].AsValueSlice() + oldOrderedListMapped := mapObjectRenamingDatabaseOrderedListItemFromValue(oldOrderedList) + newOrderedListMapped := mapObjectRenamingDatabaseOrderedListItemFromValue(newOrderedList) + + itemsToAdd, itemsToRemove := ListDiff(oldOrderedListMapped, newOrderedListMapped) + for _, removedItem := range itemsToRemove { + ObjectRenamingDatabaseInstance.OrderedList = slices.DeleteFunc(ObjectRenamingDatabaseInstance.OrderedList, func(item objectRenamingDatabaseOrderedListItem) bool { + return item == removedItem + }) + } + + for _, addedItem := range itemsToAdd { + addedItem.Order = "" + ObjectRenamingDatabaseInstance.OrderedList = append(ObjectRenamingDatabaseInstance.OrderedList, addedItem) + } + + // The implementation is not complete due to mentioned issues with computed order in DiffSuppressFunc + } + + if d.HasChange("manually_ordered_list") { + invalidOperations := make([]error, 0) + updateChangelog := ObjectRenamingDatabaseChangelog{} + + oldManuallyOrderedList := d.GetRawState().AsValueMap()["manually_ordered_list"].AsValueSlice() + newManuallyOrderedList := d.GetRawConfig().AsValueMap()["manually_ordered_list"].AsValueSlice() + + oldOrders := collections.Map(oldManuallyOrderedList, func(item cty.Value) int { + result, _ := item.AsValueMap()["order"].AsBigFloat().Int64() + return int(result) + }) + maxStateOrder := slices.MaxFunc(oldOrders, func(a, b int) int { return a - b }) + finalState := make([]ObjectRenamingDatabaseManuallyOrderedListItem, 0) + + for _, oldItem := range oldManuallyOrderedList { + oldItem := oldItem.AsValueMap() + newItemIndex := slices.IndexFunc(newManuallyOrderedList, func(newItem cty.Value) bool { + return oldItem["order"].AsBigFloat().Cmp(newItem.AsValueMap()["order"].AsBigFloat()) == 0 + }) + // Here we analyze already existing items and check if they need to be updated in any way. + if newItemIndex != -1 { + newItem := newManuallyOrderedList[newItemIndex] + newName := oldItem["name"].AsString() + newType := oldItem["type"].AsString() + wasChanged := false + + if oldItem["name"].AsString() != newItem.AsValueMap()["name"].AsString() { + // Change name + newName = newItem.AsValueMap()["name"].AsString() + wasChanged = true + } + + if oldItem["type"].AsString() != newItem.AsValueMap()["type"].AsString() { + // Change type + newType = newItem.AsValueMap()["type"].AsString() + wasChanged = true + + // Check for incompatible types + if slices.Contains([]string{"TEXT", "STRING"}, oldItem["type"].AsString()) && slices.Contains([]string{"INT", "NUMBER"}, newType) || + slices.Contains([]string{"INT", "NUMBER"}, oldItem["type"].AsString()) && slices.Contains([]string{"TEXT", "STRING"}, newType) { + invalidOperations = append(invalidOperations, fmt.Errorf("unable to change item type from %s to %s", oldItem["type"].AsString(), newType)) + } + } + + itemToAdd := ObjectRenamingDatabaseManuallyOrderedListItem{ + Name: newName, + Type: newType, + } + finalState = append(finalState, itemToAdd) + + if wasChanged { + updateChangelog.Changed = append(updateChangelog.Changed, ObjectRenamingDatabaseChangelogChange{ + Before: map[string]any{ + "name": oldItem["name"].AsString(), + "type": oldItem["type"].AsString(), + }, + After: map[string]any{ + "name": itemToAdd.Name, + "type": itemToAdd.Type, + }, + }) + } + } else { + // If given order wasn't found, it means this item was removed. + updateChangelog.Removed = append(updateChangelog.Removed, map[string]any{ + "name": oldItem["name"].AsString(), + "type": oldItem["type"].AsString(), + }) + } + } + + // Here we analyze newly added items + for _, newItem := range newManuallyOrderedList { + newItem := newItem.AsValueMap() + if !slices.ContainsFunc(oldManuallyOrderedList, func(oldItem cty.Value) bool { + return oldItem.AsValueMap()["order"].AsBigFloat().Cmp(newItem["order"].AsBigFloat()) == 0 + }) { + newItemOrder, _ := newItem["order"].AsBigFloat().Int64() + itemToAdd := ObjectRenamingDatabaseManuallyOrderedListItem{ + Name: newItem["name"].AsString(), + Type: newItem["type"].AsString(), + } + + // Items can be only added at the end of the list, otherwise invalid operation will be reported. + if int(newItemOrder) > maxStateOrder { + finalState = append(finalState, itemToAdd) + updateChangelog.Added = append(updateChangelog.Added, map[string]any{ + "name": itemToAdd.Name, + "type": itemToAdd.Type, + }) + } else { + invalidOperations = append(invalidOperations, fmt.Errorf("unable to add a new item: %+v, in the middle", itemToAdd)) + } + } + } + + if len(invalidOperations) > 0 { + // Partial is essential in invalid operations because it will prevent invalid state from being saved. + // It was previously failing the tests because Terraform saves the state automatically despite errors being returned. + d.Partial(true) + return diag.FromErr(errors.Join(invalidOperations...)) + } else { + // Apply the changes. For "normal" implementation instead of sending whole state, single changes should be saved and applied here + // (places for single actions could be saved based on ObjectRenamingDatabaseInstance.Changelog modifications). + ObjectRenamingDatabaseInstance.ManuallyOrderedList = finalState + ObjectRenamingDatabaseInstance.ChangeLog = updateChangelog + } + } + + return ReadObjectRenamingListsAndSets(false)(ctx, d, meta) +} + +func ReadObjectRenamingListsAndSets(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + list := collections.Map(ObjectRenamingDatabaseInstance.List, func(t ObjectRenamingDatabaseListItem) map[string]any { + return map[string]any{ + "string": t.String, + "int": t.Int, + } + }) + if err := d.Set("list", list); err != nil { + return diag.FromErr(err) + } + + orderedList := make([]map[string]any, len(ObjectRenamingDatabaseInstance.OrderedList)) + for index, item := range ObjectRenamingDatabaseInstance.OrderedList { + orderedList[index] = map[string]any{ + "name": item.Name, + "order": strconv.Itoa(index), + } + } + if err := d.Set("ordered_list", orderedList); err != nil { + return diag.FromErr(err) + } + + itemAdded := len(ObjectRenamingDatabaseInstance.ManuallyOrderedList) > len(d.Get("manually_ordered_list").([]any)) + itemRemoved := len(ObjectRenamingDatabaseInstance.ManuallyOrderedList) < len(d.Get("manually_ordered_list").([]any)) + if withExternalChangesMarking && d.Get("manually_ordered_list") != nil && (itemAdded || itemRemoved) { + // Detecting external changes by comparing current state with external source + // Improvements: + // - When the items' length is the same, try to match items by unique combinations (like name + type in this case). + // - If items were added externally, see if the item was added at the end (valid operation) or somewhere in the middle (invalid operation). + // - If items were removed externally, see if the items remained in the same order (valid operation). + // - Handle cases where multiple external operations were done at once (e.g. added and removed an item). + return diag.FromErr(errors.New("detected external changes in manually_ordered_list")) + } + + if d.GetRawState().IsNull() { + // For the first read, let's "copy-paste" config into state + if err := setStateToValuesFromConfig(d, objectRenamingListsAndSetsSchema, []string{"manually_ordered_list"}); err != nil { + return diag.FromErr(err) + } + } else { + // For later reads, let's put external changes into the state. Because we don't get the information order + // from the external source, we have to guess it. We do it by matching first with state, later with config (if not found). + // To correctly find items and their order, you have to match by using fields that uniquely identify a given item (name + type in this case). + + manuallyOrderedList := make([]any, len(ObjectRenamingDatabaseInstance.ManuallyOrderedList)) + for index, item := range ObjectRenamingDatabaseInstance.ManuallyOrderedList { + var itemOrder int64 = -1 + + foundIndex := slices.IndexFunc(d.GetRawState().AsValueMap()["manually_ordered_list"].AsValueSlice(), func(value cty.Value) bool { + return value.AsValueMap()["name"].AsString() == item.Name && value.AsValueMap()["type"].AsString() == item.Type + }) + if foundIndex != -1 { + itemOrder, _ = d.GetRawState().AsValueMap()["manually_ordered_list"].AsValueSlice()[foundIndex].AsValueMap()["order"].AsBigFloat().Int64() + } + + if foundIndex == -1 && !d.GetRawConfig().IsNull() { + configFoundIndex := slices.IndexFunc(d.GetRawConfig().AsValueMap()["manually_ordered_list"].AsValueSlice(), func(value cty.Value) bool { + return value.AsValueMap()["name"].AsString() == item.Name && value.AsValueMap()["type"].AsString() == item.Type + }) + if configFoundIndex != -1 { + itemOrder, _ = d.GetRawConfig().AsValueMap()["manually_ordered_list"].AsValueSlice()[configFoundIndex].AsValueMap()["order"].AsBigFloat().Int64() + } + } + + manuallyOrderedList[index] = map[string]any{ + "name": item.Name, + "type": item.Type, + "order": itemOrder, + } + } + + if err := d.Set("manually_ordered_list", manuallyOrderedList); err != nil { + return diag.FromErr(err) + } + } + + return nil + } +} + +func DeleteObjectRenamingListsAndSets(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ObjectRenamingDatabaseInstance.List = nil + ObjectRenamingDatabaseInstance.OrderedList = nil + ObjectRenamingDatabaseInstance.ManuallyOrderedList = nil + d.SetId("") + + return nil +} + +func ignoreListOrderAfterFirstApply(parentKey string) schema.SchemaDiffSuppressFunc { + return func(key string, oldValue string, newValue string, d *schema.ResourceData) bool { + if strings.HasSuffix(key, ".#") { + return false + } + + // Raw state is not null after first apply + if !d.GetRawState().IsNull() { + // Parse item index from the key + keyParts := strings.Split(strings.TrimLeft(key, parentKey+"."), ".") + if len(keyParts) >= 2 { + index, err := strconv.Atoi(keyParts[0]) + if err != nil { + log.Println("[DEBUG] Failed to convert list item index: ", err) + return false + } + + newItems := d.GetRawConfig().AsValueMap()[parentKey].AsValueSlice() + if len(newItems) <= index { + // item was removed + return false + } + newItemHash := newItems[index].Hash() + + newItemWasAlreadyPresent := false + + // Try to find the same hash in the state; if found, the new item was already present, and it only changed place in the list + for _, oldItem := range d.GetRawState().AsValueMap()[parentKey].AsValueSlice() { + // Matching hashes indicate the order changed, but the item stayed in the config, so suppress the change + if oldItem.Hash() == newItemHash { + newItemWasAlreadyPresent = true + } + } + + oldItemIsStillPresent := false + + // Sizes of config and state may not be the same + if len(d.GetRawState().AsValueMap()[parentKey].AsValueSlice()) > index { + // Get the hash of the whole item from state (because it represents old value) + oldItemHash := d.GetRawState().AsValueMap()[parentKey].AsValueSlice()[index].Hash() + + // Try to find the same hash in the config; if found, the old item still exists, but changed its place in the list + for _, newItem := range d.GetRawConfig().AsValueMap()[parentKey].AsValueSlice() { + if newItem.Hash() == oldItemHash { + oldItemIsStillPresent = true + } + } + } else if newItemWasAlreadyPresent { + // Happens in cases where there's a new item at the end of the list, but it was already present, so do nothing + return true + } + + if newItemWasAlreadyPresent && oldItemIsStillPresent { + return true + } + } + } + + return false + } +} + +func ignoreOrderAfterFirstApplyWithOrderedList(parentKey string) schema.SchemaDiffSuppressFunc { + return func(key string, oldValue string, newValue string, d *schema.ResourceData) bool { + if strings.HasSuffix(key, ".#") { + return false + } + + // Raw state is not null after first apply + if !d.GetRawState().IsNull() { + // Parse item index from the key + keyParts := strings.Split(strings.TrimLeft(key, parentKey+"."), ".") + if len(keyParts) >= 2 { + index, err := strconv.Atoi(keyParts[0]) + if err != nil { + log.Println("[DEBUG] Failed to convert list item index: ", err) + return false + } + + newItem := d.GetRawConfig().AsValueMap()[parentKey].AsValueSlice()[index] + + newItemOrder := -1 + // The new order value cannot be retrieved because it's not set on config level. + // There's also no other way (most likely) to get the newly computed order value for a given item, + // making this approach not possible. + newItemOrderValue := newItem.AsValueMap()["order"] + if !newItemOrderValue.IsNull() { + newItemOrder, _ = strconv.Atoi(newItemOrderValue.AsString()) + } + + _ = newItemOrder + } + } + + return false + } +} + +func ignoreOrderAfterFirstApplyWithManuallyOrderedList(parentKey string) schema.SchemaDiffSuppressFunc { + return func(key string, oldValue string, newValue string, d *schema.ResourceData) bool { + if strings.HasSuffix(key, ".#") { + return false + } + + // Raw state is not null after first apply + if !d.GetRawState().IsNull() { + // Parse item index from the key + keyParts := strings.Split(strings.TrimLeft(key, parentKey+"."), ".") + if len(keyParts) >= 2 { + index, err := strconv.Atoi(keyParts[0]) + if err != nil { + log.Println("[DEBUG] Failed to convert list item index: ", err) + return false + } + + newItems := d.GetRawConfig().AsValueMap()[parentKey].AsValueSlice() + if len(newItems) <= index { + // item was removed + return false + } + + newItem := newItems[index] + itemWasAlreadyPresent := false + itemIsStillPresent := false + + var newItemOrder int64 + newItemOrderValue := newItem.AsValueMap()["order"] + if !newItemOrderValue.IsNull() { + newItemOrder, _ = newItemOrderValue.AsBigFloat().Int64() + } else { + // That's a new item + return false + } + + // It was already present, but we need to check the hash + for _, oldItem := range d.GetRawState().AsValueMap()[parentKey].AsValueSlice() { + oldItemOrder, _ := oldItem.AsValueMap()["order"].AsBigFloat().Int64() + if oldItemOrder == newItemOrder { + if oldItem.Hash() != newItem.Hash() { + // The item has the same order, but the values in other fields changed (different hash) + return false + } else { + itemWasAlreadyPresent = true + break + } + } + } + + // Check if a new item is indexable (with new items added at the end, it's not possible to index state value for them, because it doesn't exist yet) + if len(d.GetRawState().AsValueMap()[parentKey].AsValueSlice()) > index { + oldItem := d.GetRawState().AsValueMap()[parentKey].AsValueSlice()[index] + oldItemOrder, _ := oldItem.AsValueMap()["order"].AsBigFloat().Int64() + + // Check if this order is still present + for _, newItem := range d.GetRawConfig().AsValueMap()[parentKey].AsValueSlice() { + newItemOrder, _ := newItem.AsValueMap()["order"].AsBigFloat().Int64() + if oldItemOrder == newItemOrder { + if oldItem.Hash() != newItem.Hash() { + // The order is still present, but the values in other fields changed (different hash) + return false + } else { + itemIsStillPresent = true + break + } + } + } + } + + if itemWasAlreadyPresent && itemIsStillPresent { + return true + } + } + } + + return false + } +} diff --git a/pkg/resources/object_renaming_lists_and_sets_acceptance_test.go b/pkg/resources/object_renaming_lists_and_sets_acceptance_test.go new file mode 100644 index 0000000000..29b9169580 --- /dev/null +++ b/pkg/resources/object_renaming_lists_and_sets_acceptance_test.go @@ -0,0 +1,1217 @@ +package resources_test + +import ( + "context" + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_BasicListFlow(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigList([]map[string]any{ + {"string": "111", "int": 111}, + {"string": "222", "int": 222}, + {"string": "333", "int": 333}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "111", "int": "111"}, + {"string": "222", "int": "222"}, + {"string": "333", "int": "333"}, + }), + ), + }, + // Remove, shift, and add one item (in the middle) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "111", "int": 111}, + }, + Added: []map[string]any{ + {"string": "444", "int": 444}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "222", "int": 222}, + {"string": "444", "int": 444}, + {"string": "333", "int": 333}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "222", "int": "222"}, + {"string": "444", "int": "444"}, + {"string": "333", "int": "333"}, + }), + ), + }, + // Remove, shift, and add one item (at the end) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "222", "int": 222}, + }, + Added: []map[string]any{ + {"string": "111", "int": 111}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "444", "int": 444}, + {"string": "333", "int": 333}, + {"string": "111", "int": 111}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "444", "int": "444"}, + {"string": "333", "int": "333"}, + {"string": "111", "int": "111"}, + }), + ), + }, + // Remove, shift, and add one item (at the beginning) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "111", "int": 111}, + }, + Added: []map[string]any{ + {"string": "222", "int": 222}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "222", "int": 222}, + {"string": "333", "int": 333}, + {"string": "444", "int": 444}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "222", "int": "222"}, + {"string": "333", "int": "333"}, + {"string": "444", "int": "444"}, + }), + ), + }, + // Reorder items and add one + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Added: []map[string]any{ + {"string": "555", "int": 555}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "444", "int": 444}, + {"string": "555", "int": 555}, + {"string": "333", "int": 333}, + {"string": "222", "int": 222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "444", "int": "444"}, + {"string": "555", "int": "555"}, + {"string": "333", "int": "333"}, + {"string": "222", "int": "222"}, + }), + ), + }, + // Replace all items + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "333", "int": 333}, + {"string": "444", "int": 444}, + {"string": "222", "int": 222}, + {"string": "555", "int": 555}, + }, + Added: []map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + {"string": "3333", "int": 3333}, + {"string": "4444", "int": 4444}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + {"string": "3333", "int": 3333}, + {"string": "4444", "int": 4444}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + {"string": "3333", "int": "3333"}, + {"string": "4444", "int": "4444"}, + }), + ), + }, + // Remove a few items + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "3333", "int": 3333}, + {"string": "4444", "int": 4444}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + }), + ), + }, + // Remove all items + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "0"), + ), + }, + // Add few items + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Added: []map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + }), + ), + }, + // External changes: add item + { + PreConfig: func() { + resources.ObjectRenamingDatabaseInstance.List = append(resources.ObjectRenamingDatabaseInstance.List, resources.ObjectRenamingDatabaseListItem{ + String: "3333", + Int: 3333, + }) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "3333", "int": 3333}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + }), + ), + }, + // External changes: removed item + { + PreConfig: func() { + resources.ObjectRenamingDatabaseInstance.List = resources.ObjectRenamingDatabaseInstance.List[:1] + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Added: []map[string]any{ + {"string": "2222", "int": 2222}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + }), + ), + }, + // External changes: change item + { + PreConfig: func() { + resources.ObjectRenamingDatabaseInstance.List[1].String = "1010" + resources.ObjectRenamingDatabaseInstance.List[1].Int = 1010 + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"string": "1010", "int": 1010}, + }, + Added: []map[string]any{ + {"string": "2222", "int": 2222}, + }, + }), + }, + }, + Config: objectRenamingConfigList([]map[string]any{ + {"string": "1111", "int": 1111}, + {"string": "2222", "int": 2222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "1111", "int": "1111"}, + {"string": "2222", "int": "2222"}, + }), + ), + }, + // Add an item that is identical to another one (currently, failing because hash duplicates are not handled) + // { + // Config: objectRenamingConfigList([]map[string]any{ + // {"string": "222", "int": 222}, + // {"string": "222", "int": 222}, + // }), + // Check: resource.ComposeAggregateTestCheckFunc( + // assert.HasListItemsOrderIndependent("snowflake_object_renaming.test", "list", []map[string]string{ + // {"string": "222", "int": "222"}, + // {"string": "222", "int": "222"}, + // }), + // ), + // }, + }, + }) +} + +// This test researches the possibility of performing update instead of remove + add item +func TestAcc_ListNameUpdate(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigList([]map[string]any{ + {"name": "column1", "string": "111", "int": 111}, + {"name": "column2", "string": "222", "int": 222}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"name": "column1", "string": "111", "int": "111"}, + {"name": "column2", "string": "222", "int": "222"}, + }), + ), + }, + { + Config: objectRenamingConfigList([]map[string]any{ + {"name": "column2", "string": "222", "int": 222}, + {"name": "column1", "string": "111", "int": 111}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"name": "column2", "string": "222", "int": "222"}, + {"name": "column1", "string": "111", "int": "111"}, + }), + ), + }, + // It's hard to handle reorder + rename with this approach, + // because without any additional metadata, we cannot identify a given list item. + { + Config: objectRenamingConfigList([]map[string]any{ + {"name": "column3", "string": "222", "int": 222}, + {"name": "column1", "string": "111", "int": 111}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"name": "column3", "string": "222", "int": "222"}, + {"name": "column1", "string": "111", "int": "111"}, + }), + ), + }, + }, + }) +} + +func TestAcc_ListsWithDuplicatedItems(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + // Fails, because the SuppressDiffFunc works on the hash of individual items. + // To correctly suppress such changes, the number of repeated hashes should be counted. + t.Skip("Currently failing, because duplicated hashes are not supported.") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigList([]map[string]any{ + {"string": "111", "int": 111}, + {"string": "222", "int": 222}, + {"string": "333", "int": 333}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "111", "int": "111"}, + {"string": "222", "int": "222"}, + {"string": "333", "int": "333"}, + }), + ), + }, + // Introduce duplicates (it would be enough just to introduce only one to break the approach assumptions) + { + Config: objectRenamingConfigList([]map[string]any{ + {"string": "111", "int": 111}, + {"string": "111", "int": 111}, + {"string": "222", "int": 222}, + {"string": "222", "int": 222}, + {"string": "333", "int": 333}, + {"string": "333", "int": 333}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + assert.ContainsExactlyInAnyOrder("snowflake_object_renaming.test", "list", []map[string]string{ + {"string": "111", "int": "111"}, + {"string": "111", "int": "111"}, + {"string": "222", "int": "222"}, + {"string": "222", "int": "222"}, + {"string": "333", "int": "333"}, + {"string": "333", "int": "333"}, + }), + ), + }, + }, + }) +} + +func objectRenamingConfigList(listItems []map[string]any) string { + generateListItem := func(s string, i int) string { + return fmt.Sprintf(` + list { + string = "%[1]s" + int = %[2]d + } +`, s, i) + } + + generatedListItems := "" + for _, item := range listItems { + generatedListItems += generateListItem(item["string"].(string), item["int"].(int)) + } + + return fmt.Sprintf(` + + resource "snowflake_object_renaming" "test" { + %s + } + +`, generatedListItems) +} + +type objectRenamingPlanCheck func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) + +func (fn objectRenamingPlanCheck) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + fn(ctx, req, resp) +} + +func assertObjectRenamingDatabaseChangelogAndClearIt(changelog resources.ObjectRenamingDatabaseChangelog) plancheck.PlanCheck { + return objectRenamingPlanCheck(func(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { + if !reflect.DeepEqual(resources.ObjectRenamingDatabaseInstance.ChangeLog, changelog) { + resp.Error = fmt.Errorf("expected %+v changelog for this step, but got: %+v", changelog, resources.ObjectRenamingDatabaseInstance.ChangeLog) + } + resources.ObjectRenamingDatabaseInstance.ChangeLog.Added = nil + resources.ObjectRenamingDatabaseInstance.ChangeLog.Removed = nil + resources.ObjectRenamingDatabaseInstance.ChangeLog.Changed = nil + }) +} + +func TestAcc_SupportedActions(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + {"name": "nameThree", "type": "NUMBER", "order": 30}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "30"), + ), + }, + // Drop item (any position) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"name": "nameTwo", "type": "STRING"}, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "NUMBER", "order": 30}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "30"), + ), + }, + // Add item (at the end) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Added: []map[string]any{ + {"name": "nameFour", "type": "INT"}, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "NUMBER", "order": 30}, + {"name": "nameFour", "type": "INT", "order": 40}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "30"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameFour"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "40"), + ), + }, + // Rename item / Change item type (any position; compatible type change) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Changed: []resources.ObjectRenamingDatabaseChangelogChange{ + { + Before: map[string]any{"name": "nameOne", "type": "TEXT"}, + After: map[string]any{"name": "nameOneV2", "type": "STRING"}, + }, + { + Before: map[string]any{"name": "nameThree", "type": "NUMBER"}, + After: map[string]any{"name": "nameThreeV2", "type": "INT"}, + }, + { + Before: map[string]any{"name": "nameFour", "type": "INT"}, + After: map[string]any{"name": "nameFourV2", "type": "NUMBER"}, + }, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOneV2", "type": "STRING", "order": 10}, + {"name": "nameThreeV2", "type": "INT", "order": 30}, + {"name": "nameFourV2", "type": "NUMBER", "order": 40}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOneV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameThreeV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "30"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameFourV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "40"), + ), + }, + // Reorder items in the configuration + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{}), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameFourV2", "type": "NUMBER", "order": 40}, + {"name": "nameThreeV2", "type": "INT", "order": 30}, + {"name": "nameOneV2", "type": "STRING", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOneV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameThreeV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "30"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameFourV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "40"), + ), + }, + // (after reorder) Drop item (any position) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Removed: []map[string]any{ + {"name": "nameThreeV2", "type": "INT"}, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameFourV2", "type": "NUMBER", "order": 40}, + {"name": "nameOneV2", "type": "STRING", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOneV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameFourV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "40"), + ), + }, + // (after reorder) Add item (at the end) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Added: []map[string]any{ + {"name": "nameFive", "type": "INT"}, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameFive", "type": "INT", "order": 50}, + {"name": "nameFourV2", "type": "NUMBER", "order": 40}, + {"name": "nameOneV2", "type": "STRING", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOneV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameFourV2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "40"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameFive"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "50"), + ), + }, + // (after reorder) Rename item / Change item type (any position; compatible type change) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + PostApplyPreRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{ + Changed: []resources.ObjectRenamingDatabaseChangelogChange{ + { + Before: map[string]any{"name": "nameOneV2", "type": "STRING"}, + After: map[string]any{"name": "nameOneV10", "type": "TEXT"}, + }, + { + Before: map[string]any{"name": "nameFourV2", "type": "NUMBER"}, + After: map[string]any{"name": "nameFourV10", "type": "INT"}, + }, + { + Before: map[string]any{"name": "nameFive", "type": "INT"}, + After: map[string]any{"name": "nameFiveV10", "type": "NUMBER"}, + }, + }, + }), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameFiveV10", "type": "NUMBER", "order": 50}, + {"name": "nameFourV10", "type": "INT", "order": 40}, + {"name": "nameOneV10", "type": "TEXT", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOneV10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameFourV10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "40"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameFiveV10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "NUMBER"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "50"), + ), + }, + }, + }) +} + +func TestAcc_UnsupportedActions_AddItemsNotAtTheEnd(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameTwo", "type": "STRING", "order": 20}, + {"name": "nameOne", "type": "TEXT", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + // Add item (in the middle) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + "snowflake_object_renaming.test", + "manually_ordered_list", + tfjson.ActionUpdate, + sdk.String("[map[name:nameOne order:10 type:TEXT] map[name:nameTwo order:20 type:STRING]]"), + sdk.String("[map[name:atTheBeginning order:15 type:TEXT] map[name:nameTwo order:20 type:STRING] map[name:inTheMiddle order:17 type:INT] map[name:nameTwo order:20 type:STRING]]"), + ), + }, + // PostChecks don't apply when the expected error is set + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "atTheBeginning", "type": "TEXT", "order": 15}, + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "inTheMiddle", "type": "INT", "order": 17}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + ExpectError: regexp.MustCompile("unable to add a new item: \\{Name:atTheBeginning Type:TEXT}, in the middle\nunable to add a new item: \\{Name:inTheMiddle Type:INT}, in the middle"), + }, + // Try to go back to the original state (with flipped items in config) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{}), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + }, + }) +} + +func TestAcc_UnsupportedActions_ChangeItemTypeToIncompatibleOne(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameTwo", "type": "STRING", "order": 20}, + {"name": "nameOne", "type": "TEXT", "order": 10}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + // Change item type (incompatible change) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + "snowflake_object_renaming.test", + "manually_ordered_list", + tfjson.ActionUpdate, + sdk.String("[map[name:nameOne order:10 type:TEXT] map[name:nameTwo order:20 type:STRING]]"), + sdk.String("[map[name:nameOne order:10 type:NUMBER] map[name:nameTwo order:20 type:STRING]]"), + ), + }, + // PostChecks don't apply when the expected error is set + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "NUMBER", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + ExpectError: regexp.MustCompile("unable to change item type from TEXT to NUMBER"), + }, + // Try to go back to the original state (with flipped items in config) + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{}), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + }, + }) +} + +func TestAcc_UnsupportedActions_ExternalChange_AddNewItem(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + // Add one item externally + { + PreConfig: func() { + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList = append(resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList, resources.ObjectRenamingDatabaseManuallyOrderedListItem{ + Name: "externalItem", + Type: "INT", + }) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + "snowflake_object_renaming.test", + "manually_ordered_list", + tfjson.ActionUpdate, + sdk.String("[map[name:nameOne order:10 type:TEXT] map[name:nameTwo order:20 type:STRING]]"), + sdk.String("[map[name:nameOne order:10 type:NUMBER] map[name:nameTwo order:20 type:STRING] map[name:externalItem order:-1 type:INT]]"), + ), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "NUMBER", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + ExpectError: regexp.MustCompile("detected external changes in manually_ordered_list"), + }, + // Try to go back to the original state (after external correction) + { + PreConfig: func() { + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList = resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList[:len(resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList)-1] + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{}), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "2"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + ), + }, + }, + }) +} + +func TestAcc_UnsupportedActions_ExternalChange_RemoveItem(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "INT", "order": 30}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "3"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "30"), + ), + }, + // Remove one item externally + { + PreConfig: func() { + // Remove middle item + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList = append(resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList[:1], resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList[2]) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + "snowflake_object_renaming.test", + "manually_ordered_list", + tfjson.ActionUpdate, + sdk.String("[map[name:nameOne order:10 type:NUMBER] map[name:nameTwo order:20 type:STRING] map[name:nameThree order:30 type:INT]]"), + sdk.String("[map[name:nameOne order:10 type:NUMBER] map[name:nameThree order:30 type:INT]]"), + ), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "INT", "order": 30}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + ExpectError: regexp.MustCompile("detected external changes in manually_ordered_list"), + }, + // Try to go back to the original state (after external correction) + { + PreConfig: func() { + // Bring the middle item back + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList = []resources.ObjectRenamingDatabaseManuallyOrderedListItem{ + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList[0], + { + Name: "nameTwo", + Type: "STRING", + }, + resources.ObjectRenamingDatabaseInstance.ManuallyOrderedList[1], + } + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + assertObjectRenamingDatabaseChangelogAndClearIt(resources.ObjectRenamingDatabaseChangelog{}), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "INT", "order": 30}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "3"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "30"), + ), + }, + }, + }) +} + +func TestAcc_UnsupportedActions_ChangingTheOrderOfItem(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.EnableObjectRenamingTest) + acc.TestAccPreCheck(t) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "INT", "order": 30}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "3"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "30"), + ), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionUpdate), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 15}, + {"name": "nameThree", "type": "INT", "order": 35}, + {"name": "nameTwo", "type": "STRING", "order": 25}, + }), + ExpectError: regexp.MustCompile("unable to add a new item: \\{Name:nameOne Type:TEXT}, in the middle\nunable to add a new item: \\{Name:nameTwo Type:STRING}, in the middle"), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("snowflake_object_renaming.test", plancheck.ResourceActionNoop), + }, + }, + Config: objectRenamingConfigManuallyOrderedList([]map[string]any{ + {"name": "nameOne", "type": "TEXT", "order": 10}, + {"name": "nameThree", "type": "INT", "order": 30}, + {"name": "nameTwo", "type": "STRING", "order": 20}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.#", "3"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.name", "nameOne"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.type", "TEXT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.0.order", "10"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.name", "nameTwo"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.type", "STRING"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.1.order", "20"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.name", "nameThree"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.type", "INT"), + resource.TestCheckResourceAttr("snowflake_object_renaming.test", "manually_ordered_list.2.order", "30"), + ), + }, + }, + }) +} + +func objectRenamingConfigManuallyOrderedList(listItems []map[string]any) string { + generateListItem := func(name string, itemType string, order int) string { + return fmt.Sprintf(` +manually_ordered_list { + name = "%s" + type = "%s" + order = %d +} +`, name, itemType, order) + } + + generatedListItems := "" + for _, item := range listItems { + generatedListItems += generateListItem(item["name"].(string), item["type"].(string), item["order"].(int)) + } + + return fmt.Sprintf(` +resource "snowflake_object_renaming" "test" { + %s +} +`, generatedListItems) +}