Skip to content

Semantic Equal on Sets expects identical order of elements #1061

@briankassouf

Description

@briankassouf

Module version

% go list -m github.com/hashicorp/terraform-plugin-framework
github.com/hashicorp/terraform-plugin-framework v1.13.0

Relevant provider source code

Schema with SetNestedAttribute with a custom type CaseInsensitiveStringType that implements StringSemanticEquals.

	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				Computed:    true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
			"set_attribute": schema.SetNestedAttribute{
				Optional:    true,
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"resource_id": schema.StringAttribute{
							Required:    true,
						},
						"value": schema.StringAttribute{
							CustomType:  internaltypes.CaseInsensitiveStringType{},
							Description: "(case-insensitive) custom type",
							Required:    true,
						},
					},
				},
			},
		},
	}

Terraform Configuration Files

resource "..." "..." {
  set_attribute = [
    {
      resource_id = "id1"
      value   = "Value"
    },
    {
      resource_id = "id2"
      value   = "Value2"
    }
  ]
}

Expected Behavior

Sets should not care about the order of the elements contained within. This is how the SetValue.Equals function behaves here:

for _, elem := range s.elements {
if !other.contains(elem) {
return false
}
}

This property should hold for both Equals and Semantic Equals

Actual Behavior

Semantic Equals compares elements based on their index in the elements array and will result in plan changes if the order of elements and case of value change during Read on the Resource:

// Loop through proposed elements by delegating to the recursive semantic
// equality logic. This ensures that recursion will catch a further
// underlying element type has its semantic equality logic checked, even if
// the current element type does not implement the interface.
for idx, proposedNewValueElement := range proposedNewValueElements {
// Ensure new value always contains all of proposed new value
newValueElements[idx] = proposedNewValueElement
if idx >= len(priorValueElements) {
continue
}
elementReq := ValueSemanticEqualityRequest{
Path: req.Path.AtSetValue(proposedNewValueElement),
PriorValue: priorValueElements[idx],
ProposedNewValue: proposedNewValueElement,
}
elementResp := &ValueSemanticEqualityResponse{
NewValue: elementReq.ProposedNewValue,
}
ValueSemanticEquality(ctx, elementReq, elementResp)
resp.Diagnostics.Append(elementResp.Diagnostics...)
if resp.Diagnostics.HasError() {
return
}
if elementResp.NewValue.Equal(elementReq.ProposedNewValue) {
continue
}
updatedElements = true
newValueElements[idx] = elementResp.NewValue
}

This results in a plan output like:

  ~ resource "..." "..." {
        id                 = "..."
      ~ set_attribute = [
          - {
              - resource_id = "id1" -> null
              - value   = "value" -> null
            },
          - {
              - resource_id = "id2" -> null
              - value   = "value2" -> null
            },
          + {
              + resource_id = "id1"
              + value   = "Value"
            },
          + {
              + resource_id = "id2"
              + value   = "Value2"
            },
        ]
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions