Skip to content

Recent update to UseStateForUnknown causes errors when adding to nested objects #1197

@danafallon

Description

@danafallon

When we try to upgrade to 1.15.1, our terraform provider has a regression - it now outputs errors (Provider produced inconsistent result after apply) when users add a new entry to a nested attribute object. Each nested object has several attributes that use the UseStateForUnknown plan modifier because they're either computed fields (like uuid) or optional fields that we'll preserve existing values for if they aren't set in the terraform config.

Module version

v1.15.1

Relevant provider source code

func (r *PipelineResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		MarkdownDescription: "Artie Pipeline resource. This represents a pipeline that syncs data from a single source (e.g., Postgres) to a single destination (e.g., Snowflake).",
		Attributes: map[string]schema.Attribute{
			"uuid":                        schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
			"name":                        schema.StringAttribute{Required: true, MarkdownDescription: "The human-readable name of the pipeline. This is used only as a label and can contain any characters."},
			"tables": schema.MapNestedAttribute{
				Required:            true,
				MarkdownDescription: "A map of tables from the source database that you want to replicate to the destination.",
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"uuid":                   schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
						"name":                   schema.StringAttribute{Required: true, MarkdownDescription: "The name of the table in the source database."},
						"schema":                 schema.StringAttribute{Optional: true, Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, MarkdownDescription: "The name of the schema the table belongs to in the source database. This must be specified if your source database uses schemas (such as PostgreSQL), e.g. `public`."},
						"enable_history_mode":    schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, MarkdownDescription: "If set to true, we will create an additional table in the destination (suffixed with `__history`) to store all changes to the source table over time."},
						"is_partitioned":         schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, MarkdownDescription: "If the source table is partitioned, set this to true and we will ingest data from all of its partitions."},
						"alias":                  schema.StringAttribute{Optional: true, Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, MarkdownDescription: "An optional alias for the table. If set, this will be the name of the destination table."},
						"columns_to_exclude":     schema.ListAttribute{Optional: true, Computed: true, ElementType: types.StringType, PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, MarkdownDescription: "An optional list of columns to exclude from syncing to the destination."},
						"columns_to_include":     schema.ListAttribute{Optional: true, Computed: true, ElementType: types.StringType, PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, MarkdownDescription: "An optional list of columns to include in replication."},
						"columns_to_hash":        schema.ListAttribute{Optional: true, Computed: true, ElementType: types.StringType, PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, MarkdownDescription: "An optional list of columns to hash in the destination."},
						"skip_deletes":           schema.BoolAttribute{Optional: true, Computed: true, PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, MarkdownDescription: "If set to true, we will skip delete events for this table and only process insert and update events."},
...

Terraform Configuration Files

resource "artie_pipeline" "dev_postgres_to_snowflake" {
  name = "Postgres (dev db) to Snowflake"
  tables = {
    "public.account" = {
       name   = "account"
       schema = "public"
    },
    "public.company" = {
       name   = "company"
       schema = "public"
    },
  }
}

Adding a new table to the tables object is a common use case for our provider (when a customer wants to start replicating an additional database table in their data pipeline). With the above config for an existing pipeline, imagine adding another table:

"public.api_key" = {
   name   = "api_key"
   schema = "public"
}

Behavior as of v1.15.0

The plan output looked like this:

# artie_pipeline.dev_postgres_to_snowflake will be updated in-place
  ~ resource "artie_pipeline" "dev_postgres_to_snowflake" {
        name                               = "Postgres (dev db) to Snowflake"
      ~ tables                             = {
          + "public.api_key" = {
              + alias                  = (known after apply)
              + columns_to_exclude     = (known after apply)
              + columns_to_hash        = (known after apply)
              + columns_to_include     = (known after apply)
              + enable_history_mode    = false
              + is_partitioned         = false
              + name                   = "api_key"
              + schema                 = "public"
              + skip_deletes           = (known after apply)
              + uuid                   = (known after apply)
            },
            # (3 unchanged elements hidden)
        }
        # (10 unchanged attributes hidden)
    }

And the update succeeded with no errors.

Behavior as of v1.15.1

Now the plan output looks like this:

# artie_pipeline.dev_postgres_to_snowflake will be updated in-place
  ~ resource "artie_pipeline" "dev_postgres_to_snowflake" {
        name                               = "Postgres (dev db) to Snowflake"
      ~ tables                             = {
          + "public.api_key" = {
              + enable_history_mode = false
              + is_partitioned      = false
              + name                = "api_key"
              + schema              = "public"
            },
            # (4 unchanged elements hidden)
        }
        # (10 unchanged attributes hidden)
    }

And after the update is applied, these errors show up for every field that uses the UseStateForUnknown plan modifier and didn't have a value specified in the config:

╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].uuid: was null, but now cty.StringVal("e84b4aae-516f-477e-8529-546ac8116ea7").
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].alias: was null, but now cty.StringVal("").
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].skip_deletes: was null, but now cty.False.
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].columns_to_hash: was null, but now cty.ListValEmpty(cty.String).
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].columns_to_exclude: was null, but now cty.ListValEmpty(cty.String).
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent result after apply
│ 
│ When applying changes to artie_pipeline.dev_postgres_to_snowflake, provider "provider[\"registry.terraform.io/artie-labs/artie\"]" produced an unexpected new value: .tables["public.api_key"].columns_to_include: was null, but now cty.ListValEmpty(cty.String).
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

References

This seems to be the cause of the behavior change. The note in the PR description acknowledges that there could be use cases where this would cause a regression, and it looks like this is one 😅 The change correctly handles resources being created, but in this case the resource already exists and a nested object inside of it is what's being created.

Would it make sense to add an option to the UseStateForUnknown plan modifiers so that we can configure how they should treat null values?

Thank you for reviewing!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions