Repository: honeycombio/terraform-provider-honeycombio (go)

Issue:
Filters are returned in random order, so always trigger updates
**Versions**

```
Terraform v1.3.2
on linux_amd64
+ provider registry.terraform.io/honeycombio/honeycombio v0.11.1
```

**Steps to reproduce**

1. Create a query with multiple filters

```terraform
data "honeycombio_query_specification" "queue_email" {
  # VISUALIZE
  calculation {
    op = "SUM"
    column = "queueCount"
  }

  # WHERE
  filter {
    column = "queueCount"
    op     = ">="
    value  = "0"
  }
  filter {
    column = "message"
    op     = "contains"
    value  = "ScheduledTask: Email"
  }
  filter {
    column = "deployment"
    op     = "="
    value  = "foobar"
  }

  # 24 hours
  time_range = 60 * 60 * 24
}

resource "honeycombio_query" "queue_email" {
  dataset    = var.dataset
  query_json = data.honeycombio_query_specification.queue_email.json
}
```

2. Apply
3. Plan again: we expect a noop, but it wants to update resources because the API returned filters in a different order

```
  # honeycombio_query.queue_email must be replaced
-/+ resource "honeycombio_query" "queue_email" {
      ~ id         = "pepJMiY21Zc" -> (known after apply)
      ~ query_json = jsonencode(
          ~ {
              ~ filters      = [
                  - {
                      - column = "deployment"
                      - op     = "="
                      - value  = "foobar"
                    },
                    {
                        column = "queueCount"
                        op     = ">="
                        value  = 0
                    },
                    {
                        column = "message"
                        op     = "contains"
                        value  = "ScheduledTask: Email"
                    },
                  + {
                      + column = "deployment"
                      + op     = "="
                      + value  = "foobar"
                    },
                ]
                # (2 unchanged elements hidden)
            } # forces replacement
        )
        # (1 unchanged attribute hidden)
    }
```

Pull request:
query_spec - add 'equivalence' to suppress diffs
## Which problem is this PR solving?

- Closes #233 

## Short description of the changes

Adds the concept of equivalent (but not strictly equal) QuerySpecs to be used to suppress diffs (e.g. filters or havings in a different order).

Also, sneaks in a `ToPtr` typed function and removes all the specific `Ptr` functions (e.g. `StringPtr`, `BoolPtr`).


Patch:
diff --git a/client/query_spec.go b/client/query_spec.go
index b66d83b..118fc37 100644
--- a/client/query_spec.go
+++ b/client/query_spec.go
@@ -1,5 +1,7 @@
 package client
 
+import "reflect"
+
 // QuerySpec represents a Honeycomb query.
 //
 // API docs: https://docs.honeycomb.io/api/query-specification/
@@ -48,6 +50,71 @@ type QuerySpec struct {
 	Granularity *int `json:"granularity,omitempty"`
 }
 
+// Determines if two QuerySpecs are equivalent
+func (qs *QuerySpec) EquivalentTo(other QuerySpec) bool {
+	// The order of Calculations is important for visualization rendering, so we're looking for equality
+	calcMatch := true
+	if len(qs.Calculations) != len(other.Calculations) {
+		calcMatch = false
+	} else {
+		for i := range qs.Calculations {
+			if !reflect.DeepEqual(qs.Calculations[i], other.Calculations[i]) {
+				calcMatch = false
+				break
+			}
+		}
+	}
+	if !calcMatch {
+		// 'COUNT' is the default Calculation and equivalent to an empty Calculations -- check that before we give up
+		defaultCalc := []CalculationSpec{{Op: CalculationOpCount}}
+		if (other.Calculations != nil && reflect.DeepEqual(qs.Calculations, defaultCalc)) ||
+			(qs.Calculations != nil && reflect.DeepEqual(other.Calculations, defaultCalc)) ||
+			(len(qs.Calculations) > 1 || len(other.Calculations) > 1) {
+			return false
+		}
+	}
+
+	// the exact order of filters does not matter, but their equvalence does
+	if !Equivalent(qs.Filters, other.Filters) {
+		return false
+	}
+	if qs.FilterCombination != other.FilterCombination {
+		// 'AND' is the default and equivalent to an empty Filter Combination
+		if (qs.FilterCombination == FilterCombinationAnd && other.FilterCombination != "") ||
+			(other.FilterCombination == FilterCombinationAnd && qs.FilterCombination != "") {
+			return false
+		}
+	}
+	if !reflect.DeepEqual(qs.Breakdowns, other.Breakdowns) {
+		return false
+	}
+	if !reflect.DeepEqual(qs.Orders, other.Orders) {
+		return false
+	}
+	// the exact order of havings does not matter, but their equvalence does
+	if !Equivalent(qs.Havings, other.Havings) {
+		return false
+	}
+	if !reflect.DeepEqual(qs.Limit, other.Limit) {
+		return false
+	}
+	if !reflect.DeepEqual(qs.TimeRange, other.TimeRange) {
+		// 7200 is the default, so an unset value is equivalent to 7200
+		if (reflect.DeepEqual(qs.TimeRange, 7200) && other.TimeRange != nil) ||
+			(reflect.DeepEqual(other.TimeRange, 7200) && qs.TimeRange != nil) {
+			return false
+		}
+	}
+	if !reflect.DeepEqual(qs.StartTime, other.StartTime) || !reflect.DeepEqual(qs.EndTime, other.EndTime) {
+		return false
+	}
+	if !reflect.DeepEqual(qs.Granularity, other.Granularity) {
+		return false
+	}
+
+	return true
+}
+
 // CalculationSpec represents a calculation within a query.
 type CalculationSpec struct {
 	Op CalculationOp `json:"op"`
diff --git a/client/type_helpers.go b/client/type_helpers.go
index d8e88a5..dcfc15d 100644
--- a/client/type_helpers.go
+++ b/client/type_helpers.go
@@ -1,41 +1,48 @@
 package client
 
-// BoolPtr returns a pointer to the given bool
-func BoolPtr(v bool) *bool {
-	return &v
-}
-
-// CalculationOpPtr returns a pointer to the given CalculationOp.
-func CalculationOpPtr(v CalculationOp) *CalculationOp {
-	return &v
-}
-
-// HavingOpPtr returns a pointer to the given HavingOp.
-func HavingOpPtr(v HavingOp) *HavingOp {
-	return &v
-}
-
-// ColumnTypePtr returns a pointer to the given ColumnType.
-func ColumnTypePtr(v ColumnType) *ColumnType {
-	return &v
-}
-
-// IntPtr returns a pointer to the given int.
-func IntPtr(v int) *int {
-	return &v
-}
-
-// Int64Ptr returns a pointer to the given int64.
-func Int64Ptr(v int64) *int64 {
-	return &v
-}
-
-// SortOrderPtr returns a pointer to the given SortOrder.
-func SortOrderPtr(v SortOrder) *SortOrder {
-	return &v
-}
-
-// StringPtr returns a pointer to the given string.
-func StringPtr(v string) *string {
-	return &v
+import "reflect"
+
+// Returns a pointer to the given value
+func ToPtr[T any](v T) *T {
+	return &v
+}
+
+// Determines if two slices of the same type are equivalent
+// as opposed to equal
+//
+// For example: []string{"bob", "alice"} is equivalent but not equal to []string{"alice", "bob"}
+func Equivalent[T any](a, b []T) bool {
+	if len(a) != len(b) {
+		return false
+	}
+
+	for _, ours := range a {
+		found := false
+		for _, theirs := range b {
+			if reflect.DeepEqual(ours, theirs) {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			return false
+		}
+	}
+	// do it the other way around to make sure we're not missing a match
+	for _, theirs := range b {
+		found := false
+		for _, ours := range a {
+			if reflect.DeepEqual(theirs, ours) {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			return false
+		}
+	}
+
+	return true
 }
diff --git a/honeycombio/data_source_query_specification.go b/honeycombio/data_source_query_specification.go
index e39f58c..8687bf2 100644
--- a/honeycombio/data_source_query_specification.go
+++ b/honeycombio/data_source_query_specification.go
@@ -271,7 +271,7 @@ func extractCalculations(d *schema.ResourceData) ([]honeycombio.CalculationSpec,
 
 		c, ok := d.GetOk(fmt.Sprintf("calculation.%d.column", i))
 		if ok {
-			calculations[i].Column = honeycombio.StringPtr(c.(string))
+			calculations[i].Column = honeycombio.ToPtr(c.(string))
 		}
 
 		if calculation.Op.IsUnaryOp() && calculation.Column != nil {
@@ -315,17 +315,17 @@ func extractHavings(d *schema.ResourceData) ([]honeycombio.HavingSpec, error) {
 
 		co, ok := d.GetOk(fmt.Sprintf("having.%d.calculate_op", i))
 		if ok {
-			having.CalculateOp = honeycombio.CalculationOpPtr(honeycombio.CalculationOp(co.(string)))
+			having.CalculateOp = honeycombio.ToPtr(honeycombio.CalculationOp(co.(string)))
 		}
 
 		c, ok := d.GetOk(fmt.Sprintf("having.%d.column", i))
 		if ok {
-			having.Column = honeycombio.StringPtr(c.(string))
+			having.Column = honeycombio.ToPtr(c.(string))
 		}
 
 		op, ok := d.GetOk(fmt.Sprintf("having.%d.op", i))
 		if ok {
-			having.Op = honeycombio.HavingOpPtr(honeycombio.HavingOp(op.(string)))
+			having.Op = honeycombio.ToPtr(honeycombio.HavingOp(op.(string)))
 		}
 
 		v, ok := d.GetOk(fmt.Sprintf("having.%d.value", i))
@@ -431,12 +431,12 @@ func extractOrders(d *schema.ResourceData) []honeycombio.OrderSpec {
 
 		op, ok := d.GetOk(fmt.Sprintf("order.%d.op", i))
 		if ok {
-			order.Op = honeycombio.CalculationOpPtr(honeycombio.CalculationOp(op.(string)))
+			order.Op = honeycombio.ToPtr(honeycombio.CalculationOp(op.(string)))
 		}
 
 		c, ok := d.GetOk(fmt.Sprintf("order.%d.column", i))
 		if ok {
-			order.Column = honeycombio.StringPtr(c.(string))
+			order.Column = honeycombio.ToPtr(c.(string))
 		}
 
 		so, ok := d.GetOk(fmt.Sprintf("order.%d.order", i))
@@ -447,7 +447,7 @@ func extractOrders(d *schema.ResourceData) []honeycombio.OrderSpec {
 			//
 			// not sending to avoid constant plan diffs
 			if ov != honeycombio.SortOrderAsc {
-				order.Order = honeycombio.SortOrderPtr(ov)
+				order.Order = honeycombio.ToPtr(ov)
 			}
 		}
 
@@ -462,7 +462,7 @@ func extractOptionalInt(d *schema.ResourceData, key string) *int {
 	if !ok {
 		return nil
 	}
-	return honeycombio.IntPtr(value.(int))
+	return honeycombio.ToPtr(value.(int))
 }
 
 func extractOptionalInt64(d *schema.ResourceData, key string) *int64 {
@@ -470,5 +470,5 @@ func extractOptionalInt64(d *schema.ResourceData, key string) *int64 {
 	if !ok {
 		return nil
 	}
-	return honeycombio.Int64Ptr(int64(value.(int)))
+	return honeycombio.ToPtr(int64(value.(int)))
 }
diff --git a/honeycombio/internal/verify/query_spec.go b/honeycombio/internal/verify/query_spec.go
new file mode 100644
index 0000000..92e5024
--- /dev/null
+++ b/honeycombio/internal/verify/query_spec.go
@@ -0,0 +1,20 @@
+package verify
+
+import (
+	"encoding/json"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+func SupressEquivQuerySpecDiff(_, orig, new string, _ *schema.ResourceData) bool {
+	var qs1, qs2 client.QuerySpec
+
+	if err := json.Unmarshal([]byte(orig), &qs1); err != nil {
+		return false
+	}
+	if err := json.Unmarshal([]byte(new), &qs2); err != nil {
+		return false
+	}
+	return qs1.EquivalentTo(qs2)
+}
diff --git a/honeycombio/resource_column.go b/honeycombio/resource_column.go
index bbb43ac..10ed7d0 100644
--- a/honeycombio/resource_column.go
+++ b/honeycombio/resource_column.go
@@ -152,8 +152,8 @@ func readColumn(d *schema.ResourceData) *honeycombio.Column {
 	return &honeycombio.Column{
 		ID:          d.Id(),
 		KeyName:     d.Get("key_name").(string),
-		Hidden:      honeycombio.BoolPtr(d.Get("hidden").(bool)),
+		Hidden:      honeycombio.ToPtr(d.Get("hidden").(bool)),
 		Description: d.Get("description").(string),
-		Type:        honeycombio.ColumnTypePtr(honeycombio.ColumnType(d.Get("type").(string))),
+		Type:        honeycombio.ToPtr(honeycombio.ColumnType(d.Get("type").(string))),
 	}
 }
diff --git a/honeycombio/resource_query.go b/honeycombio/resource_query.go
index 189889b..15731c1 100644
--- a/honeycombio/resource_query.go
+++ b/honeycombio/resource_query.go
@@ -24,7 +24,7 @@ func newQuery() *schema.Resource {
 				Required:         true,
 				ForceNew:         true,
 				ValidateDiagFunc: validateQueryJSON(),
-				DiffSuppressFunc: verify.SuppressEquivJSONDiffs,
+				DiffSuppressFunc: verify.SupressEquivQuerySpecDiff,
 			},
 			"dataset": {
 				Type:     schema.TypeString,
diff --git a/honeycombio/type_helpers.go b/honeycombio/type_helpers.go
index f318cf8..fa3d9a9 100644
--- a/honeycombio/type_helpers.go
+++ b/honeycombio/type_helpers.go
@@ -361,7 +361,7 @@ func expandRecipientFilter(f []interface{}) *recipientFilter {
 	filter := f[0].(map[string]interface{})
 	name := filter["name"].(string)
 	if v, ok := filter["value"].(string); ok && v != "" {
-		value = honeycombio.StringPtr(v)
+		value = honeycombio.ToPtr(v)
 	}
 	if v, ok := filter["value_regex"].(string); ok && v != "" {
 		valRegexp = regexp.MustCompile(v)


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
index bbef1a9..9cd87f4 100644
--- a/client/board_test.go
+++ b/client/board_test.go
@@ -22,10 +22,10 @@ func TestBoards(t *testing.T) {
 		Calculations: []CalculationSpec{
 			{
 				Op:     CalculationOpAvg,
-				Column: StringPtr("duration_ms"),
+				Column: ToPtr("duration_ms"),
 			},
 		},
-		TimeRange: IntPtr(3600), // 1 hour
+		TimeRange: ToPtr(3600), // 1 hour
 	})
 	assert.NoError(t, err)
 
@@ -78,7 +78,7 @@ func TestBoards(t *testing.T) {
 					Op: CalculationOpCount,
 				},
 			},
-			TimeRange: IntPtr(7200), // 2 hours
+			TimeRange: ToPtr(7200), // 2 hours
 		})
 		assert.NoError(t, err)
 		b.ColumnLayout = BoardColumnStyleMulti
diff --git a/client/column_test.go b/client/column_test.go
index 16823d9..33e99ad 100644
--- a/client/column_test.go
+++ b/client/column_test.go
@@ -19,9 +19,9 @@ func TestColumns(t *testing.T) {
 	t.Run("Create", func(t *testing.T) {
 		data := &Column{
 			KeyName:     "column_test",
-			Hidden:      BoolPtr(false),
+			Hidden:      ToPtr(false),
 			Description: "This column is created by a test",
-			Type:        ColumnTypePtr(ColumnTypeFloat),
+			Type:        ToPtr(ColumnTypeFloat),
 		}
 		column, err = c.Columns.Create(ctx, dataset, data)
 
@@ -65,9 +65,9 @@ func TestColumns(t *testing.T) {
 		data := &Column{
 			ID:          column.ID,
 			KeyName:     "a-new-keyname",
-			Hidden:      BoolPtr(true),
+			Hidden:      ToPtr(true),
 			Description: "This is a new description",
-			Type:        ColumnTypePtr(ColumnTypeBoolean),
+			Type:        ToPtr(ColumnTypeBoolean),
 		}
 		column, err = c.Columns.Update(ctx, dataset, data)
 
diff --git a/client/query_result_test.go b/client/query_result_test.go
index 514712d..72722fa 100644
--- a/client/query_result_test.go
+++ b/client/query_result_test.go
@@ -23,7 +23,7 @@ func TestQueryResults(t *testing.T) {
 				Op: "COUNT",
 			},
 		},
-		TimeRange: IntPtr(60 * 60 * 24),
+		TimeRange: ToPtr(60 * 60 * 24),
 	})
 
 	t.Run("Create", func(t *testing.T) {
diff --git a/client/query_spec_test.go b/client/query_spec_test.go
index b835e8d..5031c0f 100644
--- a/client/query_spec_test.go
+++ b/client/query_spec_test.go
@@ -21,11 +21,11 @@ func TestQuerySpec(t *testing.T) {
 			},
 			{
 				Op:     CalculationOpHeatmap,
-				Column: StringPtr("duration_ms"),
+				Column: ToPtr("duration_ms"),
 			},
 			{
 				Op:     CalculationOpP99,
-				Column: StringPtr("duration_ms"),
+				Column: ToPtr("duration_ms"),
 			},
 		},
 		Filters: []FilterSpec{
@@ -48,26 +48,147 @@ func TestQuerySpec(t *testing.T) {
 		Breakdowns:        []string{"column_1", "column_2"},
 		Orders: []OrderSpec{
 			{
-				Column: StringPtr("column_1"),
+				Column: ToPtr("column_1"),
 			},
 			{
-				Op:    CalculationOpPtr(CalculationOpCount),
-				Order: SortOrderPtr(SortOrderDesc),
+				Op:    ToPtr(CalculationOpCount),
+				Order: ToPtr(SortOrderDesc),
 			},
 		},
 		Havings: []HavingSpec{
 			{
-				Column:      StringPtr("duration_ms"),
-				Op:          HavingOpPtr(HavingOpGreaterThan),
-				CalculateOp: CalculationOpPtr(CalculationOpP99),
+				Column:      ToPtr("duration_ms"),
+				Op:          ToPtr(HavingOpGreaterThan),
+				CalculateOp: ToPtr(CalculationOpP99),
 				Value:       1000.0,
 			},
 		},
-		Limit:       IntPtr(100),
-		TimeRange:   IntPtr(3600), // 1 hour
-		Granularity: IntPtr(60),   // 1 minute
+		Limit:       ToPtr(100),
+		TimeRange:   ToPtr(3600), // 1 hour
+		Granularity: ToPtr(60),   // 1 minute
 	}
 
 	_, err := c.Queries.Create(ctx, dataset, &query)
 	assert.NoError(t, err)
 }
+
+func TestQuerySpec_EquivalentTo(t *testing.T) {
+	tests := []struct {
+		name string
+		a, b QuerySpec
+		want bool
+	}{
+		{"Empty", QuerySpec{}, QuerySpec{}, true},
+		{
+			"Empty Defaults",
+			QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: "COUNT",
+					},
+				},
+				FilterCombination: "AND",
+				TimeRange:         ToPtr(7200),
+			},
+			QuerySpec{},
+			true,
+		},
+		{
+			"Equivalent but shuffled",
+			QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op:     "HEATMAP",
+						Column: ToPtr("duration_ms"),
+					},
+					{
+						Op: "COUNT",
+					},
+				},
+				Filters: []FilterSpec{
+					{
+						Column: "colA",
+						Op:     "=",
+						Value:  "a",
+					},
+					{
+						Column: "colC",
+						Op:     "=",
+						Value:  "c",
+					},
+					{
+						Column: "colB",
+						Op:     "=",
+						Value:  "b",
+					},
+				},
+				Breakdowns: []string{"colB", "colA"},
+				TimeRange:  ToPtr(7200),
+			},
+			QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op:     "HEATMAP",
+						Column: ToPtr("duration_ms"),
+					},
+					{
+						Op: "COUNT",
+					},
+				},
+				Filters: []FilterSpec{
+					{
+						Column: "colC",
+						Op:     "=",
+						Value:  "c",
+					},
+					{
+						Column: "colB",
+						Op:     "=",
+						Value:  "b",
+					},
+					{
+						Column: "colA",
+						Op:     "=",
+						Value:  "a",
+					},
+				},
+				Breakdowns:        []string{"colB", "colA"},
+				FilterCombination: "AND",
+			},
+			true,
+		},
+		{
+			"Calculation order matters",
+			QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op:     "HEATMAP",
+						Column: ToPtr("duration_ms"),
+					},
+					{
+						Op: "COUNT",
+					},
+				},
+			},
+			QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: "COUNT",
+					},
+					{
+						Op:     "HEATMAP",
+						Column: ToPtr("duration_ms"),
+					},
+				},
+			},
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.a.EquivalentTo(tt.b); got != tt.want {
+				t.Errorf("QuerySpec.EquivalentTo() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/client/query_test.go b/client/query_test.go
index e7a6a17..ac7ccb8 100644
--- a/client/query_test.go
+++ b/client/query_test.go
@@ -24,7 +24,7 @@ func TestQueries(t *testing.T) {
 				},
 				{
 					Op:     CalculationOpHeatmap,
-					Column: StringPtr("duration_ms"),
+					Column: ToPtr("duration_ms"),
 				},
 			},
 			Filters: []FilterSpec{
@@ -42,16 +42,16 @@ func TestQueries(t *testing.T) {
 			Breakdowns:        []string{"column_1", "column_2"},
 			Orders: []OrderSpec{
 				{
-					Column: StringPtr("column_1"),
+					Column: ToPtr("column_1"),
 				},
 				{
-					Op:    CalculationOpPtr(CalculationOpCount),
-					Order: SortOrderPtr(SortOrderDesc),
+					Op:    ToPtr(CalculationOpCount),
+					Order: ToPtr(SortOrderDesc),
 				},
 			},
-			Limit:       IntPtr(100),
-			TimeRange:   IntPtr(3600), // 1 hour
-			Granularity: IntPtr(60),   // 1 minute
+			Limit:       ToPtr(100),
+			TimeRange:   ToPtr(3600), // 1 hour
+			Granularity: ToPtr(60),   // 1 minute
 		}
 
 		query, err = c.Queries.Create(ctx, dataset, data)
diff --git a/client/trigger_test.go b/client/trigger_test.go
index 90b371c..3b89a16 100644
--- a/client/trigger_test.go
+++ b/client/trigger_test.go
@@ -29,7 +29,7 @@ func TestTriggers(t *testing.T) {
 				Calculations: []CalculationSpec{
 					{
 						Op:     CalculationOpP99,
-						Column: StringPtr("duration_ms"),
+						Column: ToPtr("duration_ms"),
 					},
 				},
 				Filters: []FilterSpec{
@@ -70,7 +70,7 @@ func TestTriggers(t *testing.T) {
 		data.AlertType = trigger.AlertType
 		data.Recipients[0].ID = trigger.Recipients[0].ID
 		// set default time range
-		data.Query.TimeRange = IntPtr(300)
+		data.Query.TimeRange = ToPtr(300)
 
 		// set the default alert type
 		data.AlertType = TriggerAlertTypeValueOnChange
@@ -158,7 +158,7 @@ func TestMatchesTriggerSubset(t *testing.T) {
 						Op: CalculationOpCount,
 					},
 				},
-				Limit: IntPtr(100),
+				Limit: ToPtr(100),
 			},
 			expectedErr: errors.New("limit is not allowed in a trigger query"),
 		},
@@ -171,7 +171,7 @@ func TestMatchesTriggerSubset(t *testing.T) {
 				},
 				Orders: []OrderSpec{
 					{
-						Column: StringPtr("duration_ms"),
+						Column: ToPtr("duration_ms"),
 					},
 				},
 			},
diff --git a/client/type_helpers_test.go b/client/type_helpers_test.go
new file mode 100644
index 0000000..b76fb3e
--- /dev/null
+++ b/client/type_helpers_test.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+	"testing"
+)
+
+func TestEquivalent(t *testing.T) {
+	type Person struct {
+		Name string
+		Age  int
+	}
+
+	tests := []struct {
+		name string
+		a, b []any
+		want bool
+	}{
+		{"True - Identical", []any{"bob", "alice", "carol"}, []any{"bob", "alice", "carol"}, true},
+		{"True - Out of order", []any{1, 2, 3}, []any{2, 3, 1}, true},
+		{"True - Equiv Structs out of order", []any{Person{"Bob", 42}, Person{"Carol", 32}, Person{"Mallory", 25}}, []any{Person{"Mallory", 25}, Person{"Carol", 32}, Person{"Bob", 42}}, true},
+		{"False - Not the same length", []any{1, 2, 3, 4}, []any{2, 3, 1}, false},
+		{"False - Different values", []any{Person{"Bob", 42}, Person{"Carol", 32}, Person{"Mallory", 25}}, []any{Person{"Judy", 47}, Person{"Olivia", 27}, Person{"Frank", 21}}, false},
+		{"False - Different cases", []any{"Bob", "Alice", "Carol"}, []any{"bob", "alice", "carol"}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := Equivalent(tt.a, tt.b); got != tt.want {
+				t.Errorf("Equivalent() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/honeycombio/resource_query_test.go b/honeycombio/resource_query_test.go
index df3b719..b684bb1 100644
--- a/honeycombio/resource_query_test.go
+++ b/honeycombio/resource_query_test.go
@@ -84,7 +84,7 @@ func testAccCheckQueryExists(t *testing.T, dataset string, name string, duration
 			Calculations: []honeycombio.CalculationSpec{
 				{
 					Op:     honeycombio.CalculationOpAvg,
-					Column: honeycombio.StringPtr("duration_ms"),
+					Column: honeycombio.ToPtr("duration_ms"),
 				},
 			},
 			Filters: []honeycombio.FilterSpec{
@@ -94,7 +94,7 @@ func testAccCheckQueryExists(t *testing.T, dataset string, name string, duration
 					Value:  float64(duration),
 				},
 			},
-			TimeRange: honeycombio.IntPtr(7200),
+			TimeRange: honeycombio.ToPtr(7200),
 		}
 
 		ok = assert.Equal(t, expectedQuery, createdQuery)