Number value read from tf files is different from directly building one

I’m using terraform-plugin-framework to read data from tf file through the function
request.State.Get(ctx, &state) , but it seems that the number value (10.4) becomes weird (different from the one generated by types.NumberValue(big.NewFloat(10.4)))
and if I set the state attribute as 10.4 manually using response.State.Set(ctx, state), terraform plan test would fail since the two numbers are not equal.

Does anyone meet similar issue or any suggestion?

Hi @NullCx :wave:t6:,
Sorry that you’re running into trouble here. In order to help better understand the specific problem, can you provide the Terraform configuration and the .tfstate file value for your attribute? Also is the attribute type in the terraform-plugin-framework a Float64Attribute, Float32Attribute, or a NumberAttribute?

The number handling in both Terraform core and the plugin framework involve a lot of conversions between backing types, so it’s hard to get a complete picture without seeing the specific number that’s being stored in the Terraform state file.

Hello @SBGoods ,
The attribute type is NumberAttribute. And I can’t see anything wrong with the tfstate:

"instances": [
        {
          "schema_version": 0,
          "attributes": {
	    ...
            "thresholds": {
	      ...
              "percent_advisory": 10.4,
	      ...
            },
......

tf file:

resource "powerscale_quota" "example" {
	...
	thresholds = {
		percent_advisory = 10.4
        	...
	}
	...
}

The thing is that:

// state is a structure for handling tf config
diags := request.State.Get(ctx, &state)

// threshold is object val and percent_advisory is an attribute of Number in schema definition
// The value should be true, but it is acutally false
boolVal := state.Thresholds.Attributes()["percent_advisory"].Equal(types.NumberValue(big.NewFloat(10.4)))
...

UPDATE:

Looks the value from d.TerraformValue is the same as the value in state:

So is it possible that the value in ftypes.Value (unmarshall from tf state file maybe?) is different from the one built by types.NumberValue(big.NewFloat()) ?

Hi @NullCx,

Terraform Core builds floating point numbers from Config and State by parsing them as Strings into a big.Float type at a higher precision than if you created a floating point number with a float literal in Go using big.NewFloat(). The equals method is comparing the exact big.Float objects which is why it’s returning false.

The types.float32 and types.float64 framework types has a semantic equality method that converts the big.Float number into a Go float32 or float64 type before comparing the values. It looks like we might need to implement something similar for number types in the framework.

Another question for you, when you set the state attribute as 10.4 manually, are you receiving an error from Terraform that looks like:

Error: Provider produced inconsistent result after apply
       When applying changes to framework_number_precision.test, provider
        "provider[\"registry.terraform.io/hashicorp/framework\"]" produced an
        unexpected new value: .object_attribute.number_attribute: was
        cty.MustParseNumberVal("10.4"), but now cty.NumberFloatVal(10.4).
        
        This is a bug in the provider, which should be reported in the provider's own
        issue tracker.

If so, then this is likely a bug in the framework.

Actually no, only non-empty plan reported.

Ah I see. We still shouldn’t be getting a plan difference from Terraform but at least you are not getting a Terraform Core error.

I’m trying to write a test to replicate the behavior that you’re seeing and I haven’t been able to get a non-empty plan error yet. I’ve defined an object attribute with a number attribute in my schema:

"object_attribute": schema.ObjectAttribute{
				Optional: true,
				Computed: true,
				AttributeTypes: map[string]attr.Type{
					"number_attribute": types.NumberType,
				},
			},

And in the Read() method, I set the state manually to 10.4:

func (r NumberPrecisionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var data NumberPrecisionResourceModel

	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.ObjectAttribute, _ = types.ObjectValue(map[string]attr.Type{
		"number_attribute": types.NumberType,
	}, map[string]attr.Value{
		"number_attribute": types.NumberValue(big.NewFloat(10.4)),
	})

	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

And run the following test (Note: provider developers do not normally need to run this comprehensive of a test, this is just to look for bugs in the framework and testing module):

func TestSchemaResource_NumberAttribute_test(t *testing.T) {
	resource.UnitTest(t, resource.TestCase{
		ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
			"framework": providerserver.NewProtocol5WithError(New()),
		},
		Steps: []resource.TestStep{
			{
				Config: `resource "framework_number_precision" "test" {
					object_attribute = {
						number_attribute = 10.4
					}
				}`,
				ConfigPlanChecks: resource.ConfigPlanChecks{
					PreApply: []plancheck.PlanCheck{
						plancheck.ExpectResourceAction("framework_number_precision.test", plancheck.ResourceActionCreate),
					},
					PostApplyPreRefresh: []plancheck.PlanCheck{
						plancheck.ExpectResourceAction("framework_number_precision.test", plancheck.ResourceActionNoop),
					},
					PostApplyPostRefresh: []plancheck.PlanCheck{
						plancheck.ExpectResourceAction("framework_number_precision.test", plancheck.ResourceActionNoop),
					},
				},
				ConfigStateChecks: []statecheck.StateCheck{
					statecheck.ExpectKnownValue("framework_number_precision.test", tfjsonpath.New("object_attribute").AtMapKey("number_attribute"), knownvalue.Float32Exact(10.4)),
				},
				Check: resource.ComposeAggregateTestCheckFunc(
					resource.TestCheckResourceAttr("framework_number_precision.test", "object_attribute.number_attribute", "10.4"),
				),
			},
		},
	})
}

And the above test still passes. Is there something in my test or schema that doesn’t match what you have?

@SBGoods Yes, this schema would have no failure, but you can still find sdk.framework: Detected value change between proposed new state and prior state in log.

If you need to reproduce the AT failure, you can add one string attribute like this:

	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"threshold": schema.SingleNestedAttribute{
				Optional: true,
				Computed: true,
				Attributes: map[string]schema.Attribute{
					"threshold_pct": schema.NumberAttribute{
						Computed: true,
						Optional: true,
					},
				},
			},
			"str_val": schema.StringAttribute{
				Optional: true,
				Computed: true,
			},
		},
	}

And specify in config:

resource "framework_number_precision" "test" {
	threshold = {
		threshold_pct = 10.4
	}
	string_val = "test"
}

This might be helpful. Please try this. Thanks a lot.