Repository: honeycombio/terraform-provider-honeycombio
Pull request #689 (2025-06-02 17:09:58)

feat: Enhance `detail_filter` support for multi-value and multi-filter
## Which problem is this PR solving?

Closes: 

- https://github.com/honeycombio/terraform-provider-honeycombio/issues/684
- https://github.com/honeycombio/terraform-provider-honeycombio/issues/685

## Short description of the changes
- **Support for Multiple Filters**: multiple `detail_filter` blocks can be defined to support filtering on more than 1 field. These filters are currently combined using the `AND` conditional logic. `OR` is not currently supported

- **Multi-Value Filtering**: Each `detail_filter` block now accepts filtering all fields from a resource data source. 

- **Operator**: Introduced a new `operator` field that determines how to filter on a specific field. The supported operators are:
```
"equals", "=", "eq", "not-equals", "!=", "ne", "contains", "in", "does-not-contain",
"not-in", "starts-with", "does-not-start-with", "ends-with", "does-not-end-with",
">", "gt", ">=", "ge", "<", "lt", "<=", "le", "does-not-exist", "does-not-exist"
```

## How to verify that this has the expected result

1. Create a resource. Eg. SLO
2. Using TF, read the values of the resource using a data source with a single and multiple filters.
eg.

```
data "honeycombio_slos" "numeric_range" {
  dataset = "songs"
  detail_filter {
    name    = "tags"
    operator = "contains"
    value    = "production"
  }
}
```

```
data "honeycombio_slos" "numeric_range" {
  dataset = "songs"
  detail_filter {
    name    = "target_per_million"
    operator = ">"
    value    = "991000"
  }
  
  detail_filter {
    name    = "target_per_million"
    operator = "<"
    value    = "997000"
  }
}
```

Filter by Tags
```
data "honeycombio_slos" "core_team_with_staging" {
  dataset = "%[1]s"

  detail_filter {
    name     = "tags"
    operator = "contains"
    value    = "team:core"
  }
  
  detail_filter {
    name     = "name"
    operator = "contains"
    value    = "staging"
  }
}
```


Patch:
diff --git a/docs/data-sources/datasets.md b/docs/data-sources/datasets.md
index b634de9..dd8218a 100644
--- a/docs/data-sources/datasets.md
+++ b/docs/data-sources/datasets.md
@@ -21,12 +21,26 @@ data "honeycombio_datasets" "foo" {
 
 The following arguments are supported:
 
-* `detail_filter` - (Optional) a block to further filter results as described below. `name` must be set when providing a filter. Conflicts with `starts_with`.
+* `detail_filter` - (Optional) a block to further filter results as described below. Multiple `detail_filter` blocks can be provided to filter by multiple fields. Multiple filters are combined with a logical `AND` operation, meaning all conditions must be satisfied for a dataset to be included in the results. Conflicts with `starts_with`.
 * `starts_with` - (Optional) Deprecated: use `detail_filter` instead. Only return datasets whose name starts with the given value.
 
 To filter the results, a `detail_filter` block can be provided which accepts the following arguments:
 
-* `name` - (Required) The name of the detail field to filter by. Currently only `name` is supported.
+* `name` - (Required) The name of the detail field to filter by. This field must match a schema attribute of the `honeycombio_dataset` resource (e.g., `name`, `tags`, `description`).
+* `operator` - (Optional) The comparison operator to use for filtering. Defaults to `equals`. Valid operators include:
+  * `equals`, `=`, `eq` - Exact match comparison
+  * `not-equals`, `!=`, `ne` - Inverse exact match comparison
+  * `contains`, `in` - Substring inclusion check
+  * `does-not-contain`, `not-in` - Inverse substring inclusion check
+  * `starts-with` - Prefix matching
+  * `does-not-start-with` - Inverse prefix matching
+  * `ends-with` - Suffix matching
+  * `does-not-end-with` - Inverse suffix matching
+  * `>`, `gt` - Numeric greater than comparison
+  * `>=`, `ge` - Numeric greater than or equal comparison
+  * `<`, `lt` - Numeric less than comparison
+  * `<=`, `le` - Numeric less than or equal comparison
+  * `does-not-exist` - Field absence check
 * `value` - (Optional) The value of the detail field to match on.
 * `value_regex` - (Optional) A regular expression string to apply to the value of the detail field to match on.
 
diff --git a/docs/data-sources/environment.md b/docs/data-sources/environment.md
index 736ceec..840a92e 100644
--- a/docs/data-sources/environment.md
+++ b/docs/data-sources/environment.md
@@ -41,11 +41,25 @@ data "honeycombio_environment" "prod" {
 The following arguments are supported:
 
 * `id` - (Optional) The ID of the Environment. Conflicts with `detail_filter`.
-* `detail_filter` - (Optional) a block to further filter results as described below. `name` must be set when providing a filter.
+* `detail_filter` - (Optional) a block to further filter results as described below. Multiple `detail_filter` blocks can be provided to filter by multiple fields. Multiple filters are combined with a logical `AND` operation, meaning all conditions must be satisfied for an environment to be included in the results.
 
 To filter the results, a `detail_filter` block can be provided which accepts the following arguments:
 
-* `name` - (Required) The name of the detail field to filter by. Currently only `name` is supported.
+* `name` - (Required) The name of the detail field to filter by. This field must match a schema attribute of the `honeycombio_environment` resource (e.g., `name`, `color`, `description`).
+* `operator` - (Optional) The comparison operator to use for filtering. Defaults to `equals`. Valid operators include:
+  * `equals`, `=`, `eq` - Exact match comparison
+  * `not-equals`, `!=`, `ne` - Inverse exact match comparison
+  * `contains`, `in` - Substring inclusion check
+  * `does-not-contain`, `not-in` - Inverse substring inclusion check
+  * `starts-with` - Prefix matching
+  * `does-not-start-with` - Inverse prefix matching
+  * `ends-with` - Suffix matching
+  * `does-not-end-with` - Inverse suffix matching
+  * `>`, `gt` - Numeric greater than comparison
+  * `>=`, `ge` - Numeric greater than or equal comparison
+  * `<`, `lt` - Numeric less than comparison
+  * `<=`, `le` - Numeric less than or equal comparison
+  * `does-not-exist` - Field absence check
 * `value` - (Optional) The value of the detail field to match on.
 * `value_regex` - (Optional) A regular expression string to apply to the value of the detail field to match on.
 
diff --git a/docs/data-sources/environments.md b/docs/data-sources/environments.md
index 0cde374..fed1845 100644
--- a/docs/data-sources/environments.md
+++ b/docs/data-sources/environments.md
@@ -23,11 +23,25 @@ data "honeycombio_environments" "foo" {
 
 The following arguments are supported:
 
-* `detail_filter` - (Optional) a block to further filter results as described below. `name` must be set when providing a filter.
+* `detail_filter` - (Optional) a block to further filter results as described below. Multiple `detail_filter` blocks can be provided to filter by multiple fields. Multiple filters are combined with a logical `AND` operation, meaning all conditions must be satisfied for an environment to be included in the results.
 
 To filter the results, a `detail_filter` block can be provided which accepts the following arguments:
 
-* `name` - (Required) The name of the detail field to filter by. Currently only `name` is supported.
+* `name` - (Required) The name of the detail field to filter by. This field must match a schema attribute of the `honeycombio_environment` resource (e.g., `name`, `color`, `description`).
+* `operator` - (Optional) The comparison operator to use for filtering. Defaults to `equals`. Valid operators include:
+  * `equals`, `=`, `eq` - Exact match comparison
+  * `not-equals`, `!=`, `ne` - Inverse exact match comparison
+  * `contains`, `in` - Substring inclusion check
+  * `does-not-contain`, `not-in` - Inverse substring inclusion check
+  * `starts-with` - Prefix matching
+  * `does-not-start-with` - Inverse prefix matching
+  * `ends-with` - Suffix matching
+  * `does-not-end-with` - Inverse suffix matching
+  * `>`, `gt` - Numeric greater than comparison
+  * `>=`, `ge` - Numeric greater than or equal comparison
+  * `<`, `lt` - Numeric less than comparison
+  * `<=`, `le` - Numeric less than or equal comparison
+  * `does-not-exist` - Field absence check
 * `value` - (Optional) The value of the detail field to match on.
 * `value_regex` - (Optional) A regular expression string to apply to the value of the detail field to match on.
 
diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md
index d03f226..b0aa68a 100644
--- a/docs/data-sources/slos.md
+++ b/docs/data-sources/slos.md
@@ -24,6 +24,12 @@ data "honeycombio_slos" "foo" {
     name        = "name"
     value_regex = "foo_*"
   }
+
+  detail_filter {
+    name     = "tags"
+    operator = "contains"
+    value    = "team:core"
+  }
 }
 ```
 
@@ -32,15 +38,29 @@ data "honeycombio_slos" "foo" {
 The following arguments are supported:
 
 * `dataset` - (Optional) The dataset to retrieve the SLOs list from. If omitted, the lookup will be Environment-wide.
-* `detail_filter` - (Optional) a block to further filter results as described below. `name` must be set when providing a filter.
+* `detail_filter` - (Optional) a block to further filter results as described below. Multiple `detail_filter` blocks can be provided to filter by multiple fields. Multiple filters are combined with a logical `AND` operation, meaning all conditions must be satisfied for an SLO to be included in the results.
 
 To further filter the SLO results, a `detail_filter` block can be provided which accepts the following arguments:
 
-* `name` - (Required) The name of the detail field to filter by. Currently only `name` is supported.
+* `name` - (Required) The name of the detail field to filter by. This field must match a schema attribute of the `honeycombio_slo` resource (e.g., `name`, `tags`, `description`).
+* `operator` - (Optional) The comparison operator to use for filtering. Defaults to `equals`. Valid operators include:
+  * `equals`, `=`, `eq` - Exact match comparison
+  * `not-equals`, `!=`, `ne` - Inverse exact match comparison
+  * `contains`, `in` - Substring inclusion check
+  * `does-not-contain`, `not-in` - Inverse substring inclusion check
+  * `starts-with` - Prefix matching
+  * `does-not-start-with` - Inverse prefix matching
+  * `ends-with` - Suffix matching
+  * `does-not-end-with` - Inverse suffix matching
+  * `>`, `gt` - Numeric greater than comparison
+  * `>=`, `ge` - Numeric greater than or equal comparison
+  * `<`, `lt` - Numeric less than comparison
+  * `<=`, `le` - Numeric less than or equal comparison
+  * `does-not-exist` - Field absence check
 * `value` - (Optional) The value of the detail field to match on.
 * `value_regex` - (Optional) A regular expression string to apply to the value of the detail field to match on.
 
-~> **Note** one of `value` or `value_regex` is required.
+~> **Note:** Either `value` or `value_regex` must be specified, but not both.
 
 ## Attribute Reference
 
diff --git a/honeycombio/type_helpers.go b/honeycombio/type_helpers.go
index 47d91e0..52948cd 100644
--- a/honeycombio/type_helpers.go
+++ b/honeycombio/type_helpers.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"regexp"
-	"strconv"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -13,22 +12,6 @@ import (
 	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
-func coerceValueToType(i string) interface{} {
-	// HCL really has three base types: bool, string, and number
-	// The Plugin SDK allows typing a schema field to Int or Float
-
-	// Plugin SDK assumes 64bit so we'll do the same
-	if v, err := strconv.ParseInt(i, 10, 64); err == nil {
-		return v
-	} else if v, err := strconv.ParseFloat(i, 64); err == nil {
-		return v
-	} else if v, err := strconv.ParseBool(i); err == nil {
-		return v
-	}
-	// fallthrough to string
-	return i
-}
-
 // getDatasetOrAll returns the dataset from the resource data.
 // If the dataset is empty, it returns the 'magic' EnvironmentWideSlug `__all__`.
 func getDatasetOrAll(d *schema.ResourceData) string {
diff --git a/internal/helper/coerce/coerce.go b/internal/helper/coerce/coerce.go
new file mode 100644
index 0000000..fcf390e
--- /dev/null
+++ b/internal/helper/coerce/coerce.go
@@ -0,0 +1,56 @@
+package coerce
+
+import (
+	"fmt"
+
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+// ValueToType converts a string value to its appropriate type.
+func ValueToType(i string) interface{} {
+	// HCL really has three base types: bool, string, and number
+	// The Plugin SDK allows typing a schema field to Int or Float
+
+	// Plugin SDK assumes 64bit so we'll do the same
+	if v, err := strconv.ParseInt(i, 10, 64); err == nil {
+		return v
+	} else if v, err := strconv.ParseFloat(i, 64); err == nil {
+		return v
+	} else if v, err := strconv.ParseBool(i); err == nil {
+		return v
+	}
+	// fallthrough to string
+	return i
+}
+
+// ValueToString converts a value of any type to its string representation.
+// It handles basic types like string, int, float, and bool.
+// For other types (like maps, slices, etc.), it uses fmt.Sprintf to convert to string.
+func ValueToString(value interface{}) string {
+	switch v := value.(type) {
+	case string:
+		return v
+	case int, int32, int64:
+		return fmt.Sprintf("%d", v)
+	case float32, float64:
+		return fmt.Sprintf("%f", v)
+	case bool:
+		return fmt.Sprintf("%t", v)
+	case basetypes.StringValue:
+		// Handle Terraform's StringValue type.
+		// Calling v.String() to get the string representation
+		// returns a string with escaped quotes so we use
+		// ValueString() instead
+		return v.ValueString()
+	case fmt.Stringer:
+		// If the value implements fmt.Stringer, use its String method.
+		// i.e. If the value is a struct or a type that has a String() method
+		return v.String()
+	default:
+		// For other complex types (maps, slices, arrays, etc.), convert to string
+		// TODO: handle more complex types like maps, slices, etc. in a more structured way
+		return fmt.Sprintf("%v", v)
+	}
+}
diff --git a/internal/helper/filter/detail_filter.go b/internal/helper/filter/detail_filter.go
index 62c5bd1..fa74468 100644
--- a/internal/helper/filter/detail_filter.go
+++ b/internal/helper/filter/detail_filter.go
@@ -2,54 +2,237 @@ package filter
 
 import (
 	"fmt"
+	"reflect"
 	"regexp"
+	"strconv"
+	"strings"
 
-	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/coerce"
 )
 
+// DetailFilter provides filtering capabilities for resources.
 type DetailFilter struct {
-	Type       string
-	Value      *string
+	Field      string
+	Operator   string
+	Value      string
 	ValueRegex *regexp.Regexp
 }
 
-func NewDetailFilter(filterType, v, r string) (*DetailFilter, error) {
-	if filterType != "name" {
-		return nil, fmt.Errorf("only name is supported as a filter type")
-	}
-	if v != "" && r != "" {
-		return nil, fmt.Errorf("only one of value or value_regex may be provided")
-	}
-	if v == "" && r == "" {
-		return nil, fmt.Errorf("one of value or value_regex must be provided")
-	}
-
-	var value *string
-	var valRegexp *regexp.Regexp
-	if v != "" {
-		value = helper.ToPtr(v)
-	}
-	if r != "" {
-		valRegexp = regexp.MustCompile(r)
+func NewDetailFilter(field, operator, value, regex string) (*DetailFilter, error) {
+	var valRegex *regexp.Regexp
+	if regex != "" {
+		var err error
+		valRegex, err = regexp.Compile(regex)
+		if err != nil {
+			return nil, fmt.Errorf("invalid regex: %v", err)
+		}
 	}
 
 	return &DetailFilter{
-		Type:       filterType,
+		Field:      field,
+		Operator:   operator,
 		Value:      value,
-		ValueRegex: valRegexp,
+		ValueRegex: valRegex,
 	}, nil
 }
 
-func (f *DetailFilter) MatchName(name string) bool {
-	// nil filter fails open
+// Match determines if a resource matches the filter criteria
+func (f *DetailFilter) Match(resource interface{}) bool {
 	if f == nil {
 		return true
 	}
-	if f.Value != nil {
-		return name == *f.Value
+
+	fieldValue, found := getFieldValue(resource, f.Field)
+	if !found {
+		return false
+	}
+
+	strValue := coerce.ValueToString(fieldValue)
+
+	// For tag fields, convert to "key:value" format strings before comparison
+	// eg. "tags" field with value {"env": "prod", "team": "ops"} becomes "env:prod,team:ops"
+	if strings.ToLower(f.Field) == "tags" {
+		strValue = formatTagsAsString(fieldValue)
 	}
-	if f.ValueRegex != nil {
-		return f.ValueRegex.MatchString(name)
+
+	return compareValues(strValue, f.Operator, f.Value, f.ValueRegex)
+}
+
+// formatTagsAsString converts tag fields to a string representation in "key:value" format
+func formatTagsAsString(tagField interface{}) string {
+	v := reflect.ValueOf(tagField)
+	if v.Kind() == reflect.Ptr && !v.IsNil() {
+		v = v.Elem()
+	}
+
+	var tagPairs []string
+
+	switch v.Kind() {
+	// Tags should typically come back as a slice but adding support for maps
+	// just in case.
+	case reflect.Map:
+		for _, key := range v.MapKeys() {
+			keyStr := fmt.Sprintf("%v", key.Interface())
+			valueStr := fmt.Sprintf("%v", v.MapIndex(key).Interface())
+			tagPairs = append(tagPairs, fmt.Sprintf("%s:%s", keyStr, valueStr))
+		}
+	case reflect.Slice, reflect.Array:
+		for i := range v.Len() {
+			item := v.Index(i).Interface()
+
+			// Check if it's a string (simple tag)
+			if tagStr, ok := item.(string); ok {
+				tagPairs = append(tagPairs, tagStr)
+				continue
+			}
+
+			itemValue := reflect.ValueOf(item)
+			if itemValue.Kind() == reflect.Struct {
+				keyField := itemValue.FieldByName("Key")
+				valueField := itemValue.FieldByName("Value")
+
+				if keyField.IsValid() && valueField.IsValid() {
+					key := fmt.Sprintf("%v", keyField.Interface())
+					value := fmt.Sprintf("%v", valueField.Interface())
+					tagPairs = append(tagPairs, fmt.Sprintf("%s:%s", key, value))
+				}
+			}
+		}
+	case reflect.Struct:
+		if mapValue, ok := tagField.(basetypes.MapValue); ok {
+			elements := mapValue.Elements()
+			for k, v := range elements {
+				if strValue, ok := v.(basetypes.StringValue); ok {
+					tagPairs = append(tagPairs, fmt.Sprintf("%s:%s", k, strValue.ValueString()))
+				}
+			}
+			if len(tagPairs) > 0 {
+				return strings.Join(tagPairs, ",")
+			}
+		}
+	}
+
+	// As a fallback, if no tags were found, return the string representation of the field
+	if len(tagPairs) == 0 {
+		return coerce.ValueToString(tagField)
+	}
+
+	return strings.Join(tagPairs, ",")
+}
+
+// getFieldValue extracts a field value from a resource using reflection
+// Field name matching is always case-insensitive
+func getFieldValue(resource interface{}, fieldName string) (interface{}, bool) {
+	v := reflect.ValueOf(resource)
+	if v.Kind() == reflect.Ptr {
+		v = v.Elem()
+	}
+
+	lowerFieldName := strings.ToLower(fieldName)
+
+	// Create alternate version without underscores.
+	// This is useful for fields that might be named in camelCase or snake_case.
+	// This can occur when resources are read as structs or maps. The struct fields
+	// are typically in camelCase, while the filter field names might be in snake_case.
+	//
+	// For example, "timePeriodDays" becomes "timeperioddays" and the filter might use
+	// "time_period_days" or "timeperioddays". Underscores are removed from the field name
+	// to allow matching both camelCase and snake_case styles.
+	// This allows matching both styles.
+	noUnderscoreFieldName := strings.ReplaceAll(lowerFieldName, "_", "")
+
+	switch v.Kind() {
+	case reflect.Struct:
+		t := v.Type()
+		for i := range t.NumField() {
+			fieldType := t.Field(i)
+			fieldNameLower := strings.ToLower(fieldType.Name)
+
+			// Direct match
+			if fieldNameLower == lowerFieldName {
+				return v.Field(i).Interface(), true
+			}
+
+			// Match with underscores removed (camelCase to snake_case conversion)
+			if strings.ReplaceAll(fieldNameLower, "_", "") == noUnderscoreFieldName {
+				return v.Field(i).Interface(), true
+			}
+		}
+		return nil, false
+	case reflect.Map:
+		for _, key := range v.MapKeys() {
+			keyStr, ok := key.Interface().(string)
+			if !ok {
+				continue
+			}
+
+			if strings.ToLower(keyStr) == lowerFieldName {
+				return v.MapIndex(key).Interface(), true
+			}
+		}
+		return nil, false
+	default:
+		return nil, false
+	}
+}
+
+// compareValues compares two values using the specified operator
+func compareValues(strValue, operator, filterValue string, regex *regexp.Regexp) bool {
+	if regex != nil {
+		return regex.MatchString(strValue)
+	}
+
+	switch operator {
+	case "equals", "=", "eq", "":
+		return strValue == filterValue
+	case "not-equals", "!=", "ne":
+		return strValue != filterValue
+	case "contains", "in":
+		return strings.Contains(strValue, filterValue)
+	case "does-not-contain", "not-in":
+		return !strings.Contains(strValue, filterValue)
+	case "starts-with":
+		return strings.HasPrefix(strValue, filterValue)
+	case "does-not-start-with":
+		return !strings.HasPrefix(strValue, filterValue)
+	case "ends-with":
+		return strings.HasSuffix(strValue, filterValue)
+	case "does-not-end-with":
+		return !strings.HasSuffix(strValue, filterValue)
+	case ">", "gt":
+		// Try to convert both to numbers for comparison
+		val1, err1 := strconv.ParseFloat(strValue, 64)
+		val2, err2 := strconv.ParseFloat(filterValue, 64)
+		if err1 == nil && err2 == nil {
+			return val1 > val2
+		}
+		return false
+	case ">=", "ge":
+		val1, err1 := strconv.ParseFloat(strValue, 64)
+		val2, err2 := strconv.ParseFloat(filterValue, 64)
+		if err1 == nil && err2 == nil {
+			return val1 >= val2
+		}
+		return false
+	case "<", "lt":
+		val1, err1 := strconv.ParseFloat(strValue, 64)
+		val2, err2 := strconv.ParseFloat(filterValue, 64)
+		if err1 == nil && err2 == nil {
+			return val1 < val2
+		}
+		return false
+	case "<=", "le":
+		val1, err1 := strconv.ParseFloat(strValue, 64)
+		val2, err2 := strconv.ParseFloat(filterValue, 64)
+		if err1 == nil && err2 == nil {
+			return val1 <= val2
+		}
+		return false
+	case "does-not-exist":
+		return strValue == ""
+	default:
+		return false
 	}
-	return true
 }
diff --git a/internal/helper/filter/detail_filter_group.go b/internal/helper/filter/detail_filter_group.go
new file mode 100644
index 0000000..9e856c6
--- /dev/null
+++ b/internal/helper/filter/detail_filter_group.go
@@ -0,0 +1,65 @@
+package filter
+
+import (
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// FilterGroup represents a group of filters that are combined with AND logic
+type FilterGroup struct {
+	Filters []*DetailFilter
+}
+
+type DetailFilterModel struct {
+	Name       types.String `tfsdk:"name"`
+	Value      types.String `tfsdk:"value"`
+	Operator   types.String `tfsdk:"operator"`
+	ValueRegex types.String `tfsdk:"value_regex"`
+}
+
+func NewFilterGroup(detailFilter []DetailFilterModel) (*FilterGroup, error) {
+	if len(detailFilter) == 0 {
+		return &FilterGroup{}, nil
+	}
+
+	filters := make([]*DetailFilter, 0, len(detailFilter))
+
+	for _, filterModel := range detailFilter {
+		filter, err := filterModel.newFilter()
+		if err != nil {
+			return nil, fmt.Errorf("error creating filter: %w", err)
+		}
+		filters = append(filters, filter)
+	}
+
+	return &FilterGroup{
+		Filters: filters,
+	}, nil
+}
+
+func (m *DetailFilterModel) newFilter() (*DetailFilter, error) {
+	field := m.Name.ValueString()
+	operator := m.Operator.ValueString()
+	value := m.Value.ValueString()
+	regex := m.ValueRegex.ValueString()
+
+	return NewDetailFilter(field, operator, value, regex)
+}
+
+// Match determines if all filters in the group match the resource
+// TODO: Implement OR logic if needed in the future
+func (g *FilterGroup) Match(resource interface{}) bool {
+	if g == nil || len(g.Filters) == 0 {
+		return true
+	}
+
+	// All filters must match (AND logic)
+	for _, filter := range g.Filters {
+		if !filter.Match(resource) {
+			return false
+		}
+	}
+
+	return true
+}
diff --git a/internal/models/datasets.go b/internal/models/datasets.go
index 86f12dc..16aafbc 100644
--- a/internal/models/datasets.go
+++ b/internal/models/datasets.go
@@ -1,6 +1,10 @@
 package models
 
-import "github.com/hashicorp/terraform-plugin-framework/types"
+import (
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
+)
 
 type DatasetResourceModel struct {
 	ID              types.String `tfsdk:"id"`
@@ -14,9 +18,9 @@ type DatasetResourceModel struct {
 }
 
 type DatasetsDataSourceModel struct {
-	ID           types.String        `tfsdk:"id"`
-	StartsWith   types.String        `tfsdk:"starts_with"`
-	DetailFilter []DetailFilterModel `tfsdk:"detail_filter"`
-	Names        []types.String      `tfsdk:"names"`
-	Slugs        []types.String      `tfsdk:"slugs"`
+	ID           types.String               `tfsdk:"id"`
+	StartsWith   types.String               `tfsdk:"starts_with"`
+	DetailFilter []filter.DetailFilterModel `tfsdk:"detail_filter"`
+	Names        []types.String             `tfsdk:"names"`
+	Slugs        []types.String             `tfsdk:"slugs"`
 }
diff --git a/internal/models/detail_filter.go b/internal/models/detail_filter.go
deleted file mode 100644
index 653935a..0000000
--- a/internal/models/detail_filter.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package models
-
-import (
-	"github.com/hashicorp/terraform-plugin-framework/types"
-
-	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
-)
-
-type DetailFilterModel struct {
-	Name       types.String `tfsdk:"name"`
-	Value      types.String `tfsdk:"value"`
-	ValueRegex types.String `tfsdk:"value_regex"`
-}
-
-func (f *DetailFilterModel) NewFilter() (*filter.DetailFilter, error) {
-	if f == nil {
-		return nil, nil
-	}
-	return filter.NewDetailFilter(f.Name.ValueString(), f.Value.ValueString(), f.ValueRegex.ValueString())
-}
diff --git a/internal/models/environments.go b/internal/models/environments.go
index b11eb72..d95ff28 100644
--- a/internal/models/environments.go
+++ b/internal/models/environments.go
@@ -2,6 +2,8 @@ package models
 
 import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
 )
 
 type EnvironmentResourceModel struct {
@@ -14,17 +16,17 @@ type EnvironmentResourceModel struct {
 }
 
 type EnvironmentDataSourceModel struct {
-	ID              types.String        `tfsdk:"id"`
-	DetailFilter    []DetailFilterModel `tfsdk:"detail_filter"`
-	Name            types.String        `tfsdk:"name"`
-	Slug            types.String        `tfsdk:"slug"`
-	Description     types.String        `tfsdk:"description"`
-	Color           types.String        `tfsdk:"color"`
-	DeleteProtected types.Bool          `tfsdk:"delete_protected"`
+	ID              types.String               `tfsdk:"id"`
+	DetailFilter    []filter.DetailFilterModel `tfsdk:"detail_filter"`
+	Name            types.String               `tfsdk:"name"`
+	Slug            types.String               `tfsdk:"slug"`
+	Description     types.String               `tfsdk:"description"`
+	Color           types.String               `tfsdk:"color"`
+	DeleteProtected types.Bool                 `tfsdk:"delete_protected"`
 }
 
 type EnvironmentsDataSourceModel struct {
-	ID           types.String        `tfsdk:"id"`
-	DetailFilter []DetailFilterModel `tfsdk:"detail_filter"`
-	IDs          []types.String      `tfsdk:"ids"`
+	ID           types.String               `tfsdk:"id"`
+	DetailFilter []filter.DetailFilterModel `tfsdk:"detail_filter"`
+	IDs          []types.String             `tfsdk:"ids"`
 }
diff --git a/internal/models/slo.go b/internal/models/slo.go
index bd12c99..53b148f 100644
--- a/internal/models/slo.go
+++ b/internal/models/slo.go
@@ -2,6 +2,8 @@ package models
 
 import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/filter"
 )
 
 type SLOResourceModel struct {
@@ -17,10 +19,10 @@ type SLOResourceModel struct {
 }
 
 type SLOsDataSourceModel struct {
-	ID           types.String        `tfsdk:"id"`
-	Dataset      types.String        `tfsdk:"dataset"`
-	DetailFilter []DetailFilterModel `tfsdk:"detail_filter"`
-	IDs          []types.String      `tfsdk:"ids"`
+	ID           types.String               `tfsdk:"id"`
+	Dataset      types.String               `tfsdk:"dataset"`
+	DetailFilter []filter.DetailFilterModel `tfsdk:"detail_filter"`
+	IDs          []types.String             `tfsdk:"ids"`
 }
 
 type SLODataSourceModel struct {
diff --git a/internal/provider/datasets_data_source.go b/internal/provider/datasets_data_source.go
index b5b62e6..4e5da67 100644
--- a/internal/provider/datasets_data_source.go
+++ b/internal/provider/datasets_data_source.go
@@ -2,7 +2,7 @@ package provider
 
 import (
 	"context"
-	"regexp"
+	"time"
 
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
@@ -100,34 +100,49 @@ func (d *datasetsDataSource) Read(ctx context.Context, req datasource.ReadReques
 		return
 	}
 
-	var nameFilter *filter.DetailFilter
+	var filterGroup *filter.FilterGroup
 	if len(data.DetailFilter) > 0 {
-		nameFilter, err = data.DetailFilter[0].NewFilter()
+		var err error
+		filterGroup, err = filter.NewFilterGroup(data.DetailFilter)
 		if err != nil {
-			resp.Diagnostics.AddError("Unable to create Dataset filter", err.Error())
+			resp.Diagnostics.AddError("Unable to create Dataset filter group", err.Error())
 			return
 		}
 	} else if !data.StartsWith.IsNull() {
-		exp, err := regexp.Compile(data.StartsWith.ValueString() + "*")
-		if err != nil {
-			resp.Diagnostics.AddError("Unable to create Dataset filter", err.Error())
-			return
+		nameFilter := &filter.DetailFilter{
+			Field:    "name",
+			Operator: "starts-with",
+			Value:    data.StartsWith.ValueString(),
 		}
-		nameFilter = &filter.DetailFilter{
-			Type:       "name",
-			ValueRegex: exp,
+		filterGroup = &filter.FilterGroup{
+			Filters: []*filter.DetailFilter{nameFilter},
 		}
 	}
 
 	for _, e := range datasets {
-		if !nameFilter.MatchName(e.Name) {
-			continue
+		datasetResource := datasetToResourceModel(e)
+
+		if filterGroup == nil || filterGroup.Match(datasetResource) {
+			data.Names = append(data.Names, types.StringValue(e.Name))
+			data.Slugs = append(data.Slugs, types.StringValue(e.Slug))
 		}
-		data.Names = append(data.Names, types.StringValue(e.Name))
-		data.Slugs = append(data.Slugs, types.StringValue(e.Slug))
 	}
 	data.ID = types.StringValue(hashcode.StringValues(data.Slugs))
 
 	diags := resp.State.Set(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 }
+
+// datasetToResourceModel converts a client.Dataset to a DatasetResourceModel
+func datasetToResourceModel(ds client.Dataset) *models.DatasetResourceModel {
+	return &models.DatasetResourceModel{
+		ID:              types.StringValue(ds.Slug),
+		Slug:            types.StringValue(ds.Slug),
+		Name:            types.StringValue(ds.Name),
+		Description:     types.StringValue(ds.Description),
+		ExpandJSONDepth: types.Int32Value(int32(ds.ExpandJSONDepth)),
+		DeleteProtected: types.BoolPointerValue(ds.Settings.DeleteProtected),
+		CreatedAt:       types.StringValue(ds.CreatedAt.Format(time.RFC3339)),
+		LastWrittenAt:   types.StringValue(ds.LastWrittenAt.Format(time.RFC3339)),
+	}
+}
diff --git a/internal/provider/detail_filter.go b/internal/provider/detail_filter.go
index 2f0a3dd..7629811 100644
--- a/internal/provider/detail_filter.go
+++ b/internal/provider/detail_filter.go
@@ -1,7 +1,6 @@
 package provider
 
 import (
-	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
 	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
 	"github.com/hashicorp/terraform-plugin-framework/path"
@@ -12,14 +11,23 @@ import (
 
 func detailFilterSchema() schema.ListNestedBlock {
 	return schema.ListNestedBlock{
-		Description: "Attributes to filter the results with. `name` must be set when providing a filter.",
-		Validators:  []validator.List{listvalidator.SizeAtMost(1)},
+		Description: "Attributes to filter the results with. Multiple filters can be specified, and all conditions must be satisfied (AND logic).",
 		NestedObject: schema.NestedBlockObject{
 			Attributes: map[string]schema.Attribute{
 				"name": schema.StringAttribute{
 					Required:    true,
-					Description: "The name of the detail field to filter by. Currently only 'name' is supported.",
-					Validators:  []validator.String{stringvalidator.OneOf("name")},
+					Description: "The field to filter by (e.g., 'name', 'id', 'description', etc.).",
+				},
+				"operator": schema.StringAttribute{
+					Optional:    true,
+					Description: "The comparison operator. The default is 'equals'.",
+					Validators: []validator.String{
+						stringvalidator.OneOf(
+							"equals", "=", "eq", "not-equals", "!=", "ne", "contains", "in", "does-not-contain",
+							"not-in", "starts-with", "does-not-start-with", "ends-with", "does-not-end-with",
+							">", "gt", ">=", "ge", "<", "lt", "<=", "le", "does-not-exist", "does-not-exist",
+						),
+					},
 				},
 				"value": schema.StringAttribute{
 					Optional:    true,
diff --git a/internal/provider/environment_data_source.go b/internal/provider/environment_data_source.go
index 5b41cd4..fe11d10 100644
--- a/internal/provider/environment_data_source.go
+++ b/internal/provider/environment_data_source.go
@@ -120,11 +120,12 @@ func (d *environmentDataSource) Read(ctx context.Context, req datasource.ReadReq
 		}
 	} else {
 		// we're using the detail filter to find the environment
-		var envFilter *filter.DetailFilter
+		var filterGroup *filter.FilterGroup
 		if len(data.DetailFilter) > 0 {
-			envFilter, err = data.DetailFilter[0].NewFilter()
+			var err error
+			filterGroup, err = filter.NewFilterGroup(data.DetailFilter)
 			if err != nil {
-				resp.Diagnostics.AddError("Unable to create Environment filter", err.Error())
+				resp.Diagnostics.AddError("Unable to create Environment filter group", err.Error())
 				return
 			}
 		}
@@ -144,10 +145,11 @@ func (d *environmentDataSource) Read(ctx context.Context, req datasource.ReadReq
 
 		matched := make([]*v2client.Environment, 0, len(envs))
 		for _, e := range envs {
-			if !envFilter.MatchName(e.Name) {
-				continue
+			envResource := environmentToResourceModel(e)
+
+			if filterGroup == nil || filterGroup.Match(envResource) {
+				matched = append(matched, e)
 			}
-			matched = append(matched, e)
 		}
 
 		if len(matched) == 0 {
@@ -177,3 +179,19 @@ func (d *environmentDataSource) Read(ctx context.Context, req datasource.ReadReq
 	diags := resp.State.Set(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 }
+
+// environmentToResourceModel converts a v2client.Environment to a EnvironmentResourceModel
+func environmentToResourceModel(env *v2client.Environment) *models.EnvironmentResourceModel {
+	if env == nil {
+		return nil
+	}
+
+	return &models.EnvironmentResourceModel{
+		ID:              types.StringValue(env.ID),
+		Name:            types.StringValue(env.Name),
+		Slug:            types.StringValue(env.Slug),
+		Description:     types.StringPointerValue(env.Description),
+		Color:           types.StringPointerValue(env.Color),
+		DeleteProtected: types.BoolPointerValue(env.Settings.DeleteProtected),
+	}
+}
diff --git a/internal/provider/environments_data_source.go b/internal/provider/environments_data_source.go
index c069849..06363d7 100644
--- a/internal/provider/environments_data_source.go
+++ b/internal/provider/environments_data_source.go
@@ -89,20 +89,19 @@ func (d *environmentsDataSource) Read(ctx context.Context, req datasource.ReadRe
 		envs = append(envs, items...)
 	}
 
-	var envFilter *filter.DetailFilter
-	if len(data.DetailFilter) > 0 {
-		envFilter, err = data.DetailFilter[0].NewFilter()
-		if err != nil {
-			resp.Diagnostics.AddError("Unable to create Environment filter", err.Error())
-			return
-		}
+	// Create a filter group with all filters (implicit AND logic)
+	filterGroup, err := filter.NewFilterGroup(data.DetailFilter)
+	if err != nil {
+		resp.Diagnostics.AddError("Unable to create Environment filter group", err.Error())
+		return
 	}
 
 	for _, e := range envs {
-		if !envFilter.MatchName(e.Name) {
-			continue
+		envResource := environmentToResourceModel(e)
+
+		if filterGroup == nil || filterGroup.Match(envResource) {
+			data.IDs = append(data.IDs, types.StringValue(e.ID))
 		}
-		data.IDs = append(data.IDs, types.StringValue(e.ID))
 	}
 	data.ID = types.StringValue(hashcode.StringValues(data.IDs))
 
diff --git a/internal/provider/query_specification_data_source.go b/internal/provider/query_specification_data_source.go
index e7a5d79..b684269 100644
--- a/internal/provider/query_specification_data_source.go
+++ b/internal/provider/query_specification_data_source.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/coerce"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
@@ -275,11 +276,11 @@ func (d *querySpecDataSource) Read(ctx context.Context, req datasource.ReadReque
 				values := strings.Split(f.Value.ValueString(), ",")
 				result := make([]interface{}, len(values))
 				for i, value := range values {
-					result[i] = coerceValueToType(value)
+					result[i] = coerce.ValueToType(value)
 				}
 				filter.Value = result
 			} else {
-				filter.Value = coerceValueToType(f.Value.ValueString())
+				filter.Value = coerce.ValueToType(f.Value.ValueString())
 			}
 		}
 
@@ -483,15 +484,3 @@ func (d *querySpecDataSource) Read(ctx context.Context, req datasource.ReadReque
 	diags := resp.State.Set(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 }
-
-func coerceValueToType(i string) any {
-	if v, err := strconv.ParseInt(i, 10, 64); err == nil {
-		return v
-	} else if v, err := strconv.ParseFloat(i, 64); err == nil {
-		return v
-	} else if v, err := strconv.ParseBool(i); err == nil {
-		return v
-	}
-	// fallthrough to string
-	return i
-}
diff --git a/internal/provider/slos_data_source.go b/internal/provider/slos_data_source.go
index 1f688e1..adcb9c9 100644
--- a/internal/provider/slos_data_source.go
+++ b/internal/provider/slos_data_source.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/hashicorp/terraform-plugin-framework/datasource"
 	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@@ -88,22 +89,57 @@ func (d *slosDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
 		return
 	}
 
-	var sloFilter *filter.DetailFilter
-	if len(data.DetailFilter) > 0 {
-		sloFilter, err = data.DetailFilter[0].NewFilter()
-		if err != nil {
-			resp.Diagnostics.AddError("Unable to create SLO filter", err.Error())
-			return
-		}
+	// Create a filter group with all filters (implicit AND logic)
+	filterGroup, err := filter.NewFilterGroup(data.DetailFilter)
+	if err != nil {
+		resp.Diagnostics.AddError("Unable to create SLO filter group", err.Error())
+		return
 	}
+
 	for _, s := range slos {
-		if !sloFilter.MatchName(s.Name) {
-			continue
+		sloResource, err := sloToResourceModel(ctx, &s)
+
+		if helper.AddDiagnosticOnError(&resp.Diagnostics, "Converting SLO to resource model", err) {
+			return
+		}
+
+		// Use the full filter matching capability for any field
+		// For backward compatibility, if no filters are defined or if the filters
+		// don't specifically target a field, include the SLO
+		if filterGroup == nil || filterGroup.Match(sloResource) {
+			data.IDs = append(data.IDs, types.StringValue(s.ID))
 		}
-		data.IDs = append(data.IDs, types.StringValue(s.ID))
 	}
 	data.ID = types.StringValue(hashcode.StringValues(data.IDs))
 
 	diags := resp.State.Set(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 }
+
+// sloToResourceModel converts a client.SLO to a SLOResourceModel
+func sloToResourceModel(ctx context.Context, slo *client.SLO) (*models.SLOResourceModel, error) {
+	if slo == nil {
+		return &models.SLOResourceModel{}, nil
+	}
+
+	datasetsSet, diags := helper.DatasetSlugsToSet(ctx, slo.DatasetSlugs)
+	if diags.HasError() {
+		return &models.SLOResourceModel{}, fmt.Errorf("error converting dataset slugs to set: %v", diags)
+	}
+
+	tags, diags := helper.TagsToMap(ctx, slo.Tags)
+	if diags.HasError() {
+		return &models.SLOResourceModel{}, fmt.Errorf("error converting tags to map: %v", diags)
+	}
+
+	return &models.SLOResourceModel{
+		ID:               types.StringValue(slo.ID),
+		Name:             types.StringValue(slo.Name),
+		Description:      types.StringValue(slo.Description),
+		SLI:              types.StringValue(slo.SLI.Alias),
+		TargetPercentage: types.Float64Value(helper.PPMToFloat(slo.TargetPerMillion)),
+		TimePeriod:       types.Int64Value(int64(slo.TimePeriodDays)),
+		Datasets:         datasetsSet,
+		Tags:             tags,
+	}, nil
+}


Test patch:
diff --git a/honeycombio/type_helpers_test.go b/honeycombio/type_helpers_test.go
deleted file mode 100644
index 1d1f79b..0000000
--- a/honeycombio/type_helpers_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package honeycombio
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_coerceValueToType(t *testing.T) {
-	tests := []struct {
-		name     string
-		input    string
-		expected interface{}
-	}{
-		{
-			name:     "boolean true",
-			input:    "true",
-			expected: true,
-		},
-		{
-			name:     "boolean false",
-			input:    "false",
-			expected: false,
-		},
-		{
-			name:     "float",
-			input:    "10383.383",
-			expected: 10383.383,
-		},
-		{
-			name:     "int",
-			input:    "300",
-			expected: int64(300),
-		},
-		{
-			name:     "zero",
-			input:    "0",
-			expected: int64(0),
-		},
-		{
-			name:     "stringy number",
-			input:    "10.special",
-			expected: "10.special",
-		},
-		{
-			name:     "empty string",
-			input:    "",
-			expected: "",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			assert.Equal(t, tt.expected, coerceValueToType(tt.input))
-		})
-	}
-}
diff --git a/internal/helper/coerce/coerce_test.go b/internal/helper/coerce/coerce_test.go
new file mode 100644
index 0000000..c09448e
--- /dev/null
+++ b/internal/helper/coerce/coerce_test.go
@@ -0,0 +1,140 @@
+package coerce_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/coerce"
+)
+
+func Test_valueToType(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected interface{}
+	}{
+		{
+			name:     "boolean true",
+			input:    "true",
+			expected: true,
+		},
+		{
+			name:     "boolean false",
+			input:    "false",
+			expected: false,
+		},
+		{
+			name:     "float",
+			input:    "10383.383",
+			expected: 10383.383,
+		},
+		{
+			name:     "int",
+			input:    "300",
+			expected: int64(300),
+		},
+		{
+			name:     "zero",
+			input:    "0",
+			expected: int64(0),
+		},
+		{
+			name:     "stringy number",
+			input:    "10.special",
+			expected: "10.special",
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, coerce.ValueToType(tt.input))
+		})
+	}
+}
+
+func Test_valueToString(t *testing.T) {
+	type testStruct struct {
+		Name  string
+		Value int
+	}
+
+	tests := []struct {
+		name     string
+		input    interface{}
+		expected string
+	}{
+		{
+			name:     "string",
+			input:    "hello world",
+			expected: "hello world",
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: "",
+		},
+		{
+			name:     "int",
+			input:    42,
+			expected: "42",
+		},
+		{
+			name:     "int64",
+			input:    int64(9223372036854775807),
+			expected: "9223372036854775807",
+		},
+		{
+			name:     "float32",
+			input:    float32(3.14159),
+			expected: "3.141590",
+		},
+		{
+			name:     "float64",
+			input:    float64(3.14159265359),
+			expected: "3.141593",
+		},
+		{
+			name:     "boolean true",
+			input:    true,
+			expected: "true",
+		},
+		{
+			name:     "boolean false",
+			input:    false,
+			expected: "false",
+		},
+		{
+			name:     "slice",
+			input:    []int{1, 2, 3},
+			expected: "[1 2 3]",
+		},
+		{
+			name:     "map",
+			input:    map[string]int{"one": 1, "two": 2},
+			expected: "map[one:1 two:2]",
+		},
+		{
+			name:     "struct",
+			input:    testStruct{Name: "test", Value: 42},
+			expected: "{test 42}",
+		},
+		{
+			name:     "terraform string value",
+			input:    basetypes.NewStringValue("test.6UROptj5Vt"),
+			expected: "test.6UROptj5Vt",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := coerce.ValueToString(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/internal/helper/filter/detail_filter_group_test.go b/internal/helper/filter/detail_filter_group_test.go
new file mode 100644
index 0000000..021a342
--- /dev/null
+++ b/internal/helper/filter/detail_filter_group_test.go
@@ -0,0 +1,144 @@
+package filter
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestResource for testing filter matching
+type TestFilterResource struct {
+	Name        string
+	ID          string
+	Description string
+	Count       int
+	Active      bool
+	Tags        map[string]string
+}
+
+func TestNewFilterGroup(t *testing.T) {
+	// Test with empty filters
+	emptyFilters := []DetailFilterModel{}
+	group, err := NewFilterGroup(emptyFilters)
+	require.NoError(t, err)
+	assert.NotNil(t, group, "Expected non-nil group for empty filters")
+	assert.Empty(t, group.Filters, "Expected 0 filters for empty input")
+
+	// Test with non-empty filters
+	filter1 := DetailFilterModel{
+		Name:     types.StringValue("name"),
+		Operator: types.StringValue("equals"),
+		Value:    types.StringValue("test"),
+	}
+	filter2 := DetailFilterModel{
+		Name:     types.StringValue("id"),
+		Operator: types.StringValue("contains"),
+		Value:    types.StringValue("abc"),
+	}
+	filters := []DetailFilterModel{filter1, filter2}
+
+	group, err = NewFilterGroup(filters)
+	require.NoError(t, err)
+	assert.NotNil(t, group, "Expected non-nil group for non-empty filters")
+	assert.Len(t, group.Filters, 2, "Expected 2 filters for non-empty input")
+}
+
+func TestFilterGroup_Match(t *testing.T) {
+	resource := TestFilterResource{
+		Name:        "Test Resource",
+		ID:          "abc123",
+		Description: "This is a test resource",
+		Count:       42,
+		Active:      true,
+		Tags: map[string]string{
+			"env": "test",
+			"app": "example",
+		},
+	}
+
+	tests := []struct {
+		name    string
+		filters []DetailFilterModel
+		want    bool
+	}{
+		{
+			name:    "nil filters",
+			filters: nil,
+			want:    true,
+		},
+		{
+			name:    "empty filters",
+			filters: []DetailFilterModel{},
+			want:    true,
+		},
+		{
+			name: "single matching filter",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "Name", "equals", "Test Resource", ""),
+			},
+			want: true,
+		},
+		{
+			name: "single non-matching filter",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "Name", "equals", "Wrong Name", ""),
+			},
+			want: false,
+		},
+		{
+			name: "multiple matching filters",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "Name", "contains", "Resource", ""),
+				mustCreateFilter(t, "ID", "starts-with", "abc", ""),
+				mustCreateFilter(t, "Count", ">", "30", ""),
+			},
+			want: true,
+		},
+		{
+			name: "mixed matching and non-matching filters",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "Name", "contains", "Resource", ""),
+				mustCreateFilter(t, "ID", "equals", "wrong-id", ""), // This won't match
+			},
+			want: false,
+		},
+		{
+			name: "non-existent field filter",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "NonExistentField", "equals", "anything", ""),
+			},
+			want: false,
+		},
+		{
+			name: "regex filter",
+			filters: []DetailFilterModel{
+				mustCreateFilter(t, "Name", "", "", "Test.*"),
+			},
+			want: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			group, err := NewFilterGroup(tt.filters)
+			require.NoError(t, err)
+			got := group.Match(resource)
+
+			assert.Equal(t, tt.want, got, "FilterGroup.Match() mismatch for test case: %s", tt.name)
+		})
+	}
+}
+
+// Helper function to create a filter without having to check errors in each test case
+func mustCreateFilter(t *testing.T, field, operator, value, regex string) DetailFilterModel {
+	return DetailFilterModel{
+		Name:       types.StringValue(field),
+		Operator:   types.StringValue(operator),
+		Value:      types.StringValue(value),
+		ValueRegex: types.StringValue(regex),
+	}
+
+}
diff --git a/internal/helper/filter/detail_filter_test.go b/internal/helper/filter/detail_filter_test.go
new file mode 100644
index 0000000..3a8b09f
--- /dev/null
+++ b/internal/helper/filter/detail_filter_test.go
@@ -0,0 +1,327 @@
+package filter
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestResource is a struct for testing field matching
+type TestResource struct {
+	Name        string
+	ID          string
+	Count       int
+	Price       float64
+	IsAvailable bool
+	Tags        map[string]string
+	Scores      []int
+	Properties  map[string]interface{}
+}
+
+func TestMatch(t *testing.T) {
+	resource := TestResource{
+		Name:        "Test Resource",
+		ID:          "123456",
+		Count:       42,
+		Price:       99.99,
+		IsAvailable: true,
+		Tags: map[string]string{
+			"environment": "production",
+			"owner":       "team-a",
+			"region":      "us-west",
+		},
+		Scores: []int{85, 90, 95},
+		Properties: map[string]interface{}{
+			"nested": map[string]string{
+				"key":        "value",
+				"nested_key": "nested_value",
+			},
+			"version":    "1.0",
+			"created_at": "2023-10-01T12:00:00Z",
+			"updated_at": "2023-10-02T12:00:00Z",
+		},
+	}
+
+	tests := []struct {
+		name     string
+		field    string
+		operator string
+		value    string
+		regex    string
+		expected bool
+		useMap   bool // whether to use the map resource instead of struct
+	}{
+		// String field tests
+		{
+			name:     "String equals - match",
+			field:    "Name",
+			operator: "equals",
+			value:    "Test Resource",
+			expected: true,
+		},
+		{
+			name:     "String equals - no match",
+			field:    "Name",
+			operator: "equals",
+			value:    "Different",
+			expected: false,
+		},
+		{
+			name:     "String not_equals - match",
+			field:    "Name",
+			operator: "not-equals",
+			value:    "Different",
+			expected: true,
+		},
+		{
+			name:     "String contains - match",
+			field:    "Name",
+			operator: "contains",
+			value:    "Resource",
+			expected: true,
+		},
+		{
+			name:     "No operator - match",
+			field:    "Name",
+			value:    "Test Resource",
+			expected: true,
+		},
+		{
+			name:     "String starts_with - match",
+			field:    "Name",
+			operator: "starts-with",
+			value:    "Test",
+			expected: true,
+		},
+		{
+			name:     "String ends_with - match",
+			field:    "Name",
+			operator: "ends-with",
+			value:    "Resource",
+			expected: true,
+		},
+		{
+			name:     "String regex - match",
+			field:    "Name",
+			regex:    "Test.*",
+			expected: true,
+		},
+
+		// Integer field tests
+		{
+			name:     "Integer equals - match",
+			field:    "Count",
+			operator: "equals",
+			value:    "42",
+			expected: true,
+		},
+		{
+			name:     "Integer greater than - match",
+			field:    "Count",
+			operator: ">",
+			value:    "30",
+			expected: true,
+		},
+		{
+			name:     "Integer less than - match",
+			field:    "Count",
+			operator: "lt",
+			value:    "50",
+			expected: true,
+		},
+
+		// Float field tests
+		{
+			name:     "Float equals - match",
+			field:    "Price",
+			operator: "equals",
+			value:    "99.990000", // Note the format from fmt.Sprintf("%f", ...)
+			expected: true,
+		},
+		{
+			name:     "Float greater than - match",
+			field:    "Price",
+			operator: ">",
+			value:    "50.5",
+			expected: true,
+		},
+
+		// Boolean field tests
+		{
+			name:     "Bool equals - match",
+			field:    "IsAvailable",
+			operator: "equals",
+			value:    "true",
+			expected: true,
+		},
+		{
+			name:     "Bool equals - match",
+			field:    "is_available",
+			operator: "equals",
+			value:    "true",
+			expected: true,
+		},
+
+		// Edge cases
+		{
+			name:     "Non-existent field",
+			field:    "NonExistentField",
+			operator: "equals",
+			value:    "anything",
+			expected: false,
+		},
+
+		// Map field tests (using struct with map field)
+		{
+			name:     "Map field contains check",
+			field:    "Tags",
+			operator: "contains",
+			value:    "production",
+			expected: true,
+		},
+
+		// List field tests
+		{
+			name:     "List field contains check",
+			field:    "Scores",
+			operator: "contains",
+			value:    "90",
+			expected: true,
+		},
+
+		// Nested map fields
+		{
+			name:     "Nested map in Properties",
+			field:    "Properties",
+			operator: "contains",
+			value:    "nested",
+			expected: true,
+		},
+
+		// New operators test cases
+		{
+			name:     "String does-not-start-with - match",
+			field:    "Name",
+			operator: "does-not-start-with",
+			value:    "Wrong",
+			expected: true,
+		},
+		{
+			name:     "String does-not-start-with - no match",
+			field:    "Name",
+			operator: "does-not-start-with",
+			value:    "Test",
+			expected: false,
+		},
+		{
+			name:     "String does-not-end-with - match",
+			field:    "Name",
+			operator: "does-not-end-with",
+			value:    "Wrong",
+			expected: true,
+		},
+		{
+			name:     "String does-not-end-with - no match",
+			field:    "Name",
+			operator: "does-not-end-with",
+			value:    "Resource",
+			expected: false,
+		},
+		{
+			name:     "String does-not-contain - match",
+			field:    "Name",
+			operator: "does-not-contain",
+			value:    "Wrong",
+			expected: true,
+		},
+		{
+			name:     "String does-not-contain - no match",
+			field:    "Name",
+			operator: "does-not-contain",
+			value:    "Resource",
+			expected: false,
+		},
+		{
+			name:     "String not-in - match",
+			field:    "Name",
+			operator: "not-in",
+			value:    "Wrong",
+			expected: true,
+		},
+		{
+			name:     "String not-in - no match",
+			field:    "Name",
+			operator: "not-in",
+			value:    "Resource",
+			expected: false,
+		},
+		{
+			name:     "Integer greater_than_or_equal - match equal",
+			field:    "Count",
+			operator: ">=",
+			value:    "42",
+			expected: true,
+		},
+		{
+			name:     "Integer greater_than_or_equal - match greater",
+			field:    "Count",
+			operator: ">=",
+			value:    "30",
+			expected: true,
+		},
+		{
+			name:     "Integer greater_than_or_equal - no match",
+			field:    "Count",
+			operator: ">=",
+			value:    "50",
+			expected: false,
+		},
+		{
+			name:     "Integer less_than_or_equal - match equal",
+			field:    "Count",
+			operator: "<=",
+			value:    "42",
+			expected: true,
+		},
+		{
+			name:     "Integer less_than_or_equal - match less",
+			field:    "Count",
+			operator: "<=",
+			value:    "50",
+			expected: true,
+		},
+		{
+			name:     "Integer less_than_or_equal - no match",
+			field:    "Count",
+			operator: "<=",
+			value:    "30",
+			expected: false,
+		},
+		{
+			name:     "String does-not-exist - match for empty string",
+			field:    "Name",
+			operator: "does-not-exist",
+			value:    "",
+			expected: false,
+		},
+		{
+			name:     "NonExistentField does-not-exist - no match",
+			field:    "NonExistentField",
+			operator: "does-not-exist",
+			value:    "",
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			filter, err := NewDetailFilter(tt.field, tt.operator, tt.value, tt.regex)
+			if err != nil {
+				t.Fatalf("Failed to create filter: %v", err)
+			}
+			result := filter.Match(resource)
+
+			assert.Equal(t, tt.expected, result, "Filter match result mismatch for %s", tt.name)
+		})
+	}
+}
diff --git a/internal/provider/slos_data_source_test.go b/internal/provider/slos_data_source_test.go
index 697fbc5..7388f01 100644
--- a/internal/provider/slos_data_source_test.go
+++ b/internal/provider/slos_data_source_test.go
@@ -108,10 +108,47 @@ data "honeycombio_slos" "exact" {
     value = "%[2]s_slo1"
   }
 }
+
+# Multiple filters
+data "honeycombio_slos" "multi_filter" {
+  dataset     = "%[1]s"
+
+  detail_filter {
+    name    = "name"
+    operator = "starts-with"
+    value    = "%[2]s"
+  }
+  
+  detail_filter {
+    name    = "name"
+    operator = "ends-with"
+    value    = "slo1"
+  }
+}
+
+# Multiple filters with different names
+data "honeycombio_slos" "different_names" {
+  dataset     = "%[1]s"
+
+  detail_filter {
+    name    = "name"
+    operator = "starts-with"
+    value    = "%[2]s"
+  }
+  
+  detail_filter {
+    name    = "time_period"
+    operator = "equals"
+    value    = "30"
+  }
+}
+
 `, dataset, testPrefix),
 				Check: resource.ComposeAggregateTestCheckFunc(
 					resource.TestCheckResourceAttr("data.honeycombio_slos.regex", "ids.#", "2"),
 					resource.TestCheckResourceAttr("data.honeycombio_slos.exact", "ids.#", "1"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.multi_filter", "ids.#", "1"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.different_names", "ids.#", "2"),
 				),
 			},
 		},
@@ -137,4 +174,361 @@ data "honeycombio_slos" "regex" {
 			},
 		},
 	})
+
+	// Test for case without dataset specified
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+data "honeycombio_slos" "regex" {
+  detail_filter {
+    name        = "name"
+    value_regex = "%[1]s*"
+  }
+}
+
+# Test combined multiple operators
+data "honeycombio_slos" "combined_operators" {
+  detail_filter {
+    name    = "name"
+    operator = "contains"
+    value    = "%[1]s"
+  }
+  
+  detail_filter {
+    name    = "name"
+    operator = "not-equals"
+    value    = "%[1]s_slo3"  # This doesn't exist but ensures NOT logic works
+  }
+}
+`, testPrefix),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("data.honeycombio_slos.regex", "ids.#", "2"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.combined_operators", "ids.#", "2"),
+				),
+			},
+		},
+	})
+}
+
+// Test specifically for filter groups
+func TestAcc_SLOsDataSource_FilterGroups(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+	dataset := testAccDataset()
+	testPrefix := acctest.RandString(8)
+
+	testData := []struct {
+		SLI client.DerivedColumn
+		SLO client.SLO
+	}{
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_high",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_high_slo",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_high"},
+				TimePeriodDays:   30,
+				TargetPerMillion: 999000,
+				Description:      "High reliability SLO",
+			},
+		},
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_medium",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_medium_slo",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_medium"},
+				TimePeriodDays:   7,
+				TargetPerMillion: 995000,
+				Description:      "Medium reliability SLO",
+			},
+		},
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_low",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_low_slo",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_low"},
+				TimePeriodDays:   1,
+				TargetPerMillion: 990000,
+				Description:      "Low reliability SLO",
+			},
+		},
+	}
+
+	for i, tc := range testData {
+		sli, err := c.DerivedColumns.Create(ctx, dataset, &tc.SLI)
+		require.NoError(t, err)
+		slo, err := c.SLOs.Create(ctx, dataset, &tc.SLO)
+		require.NoError(t, err)
+
+		// update IDs for removal later
+		testData[i].SLI.ID = sli.ID
+		testData[i].SLO.ID = slo.ID
+	}
+
+	t.Cleanup(func() {
+		// remove SLOs at the of the test run
+		for _, tc := range testData {
+			c.SLOs.Delete(ctx, dataset, tc.SLO.ID)
+			c.DerivedColumns.Delete(ctx, dataset, tc.SLI.ID)
+		}
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+# Test combining different name filters
+data "honeycombio_slos" "combined_names" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name    = "name"
+    operator = "contains"
+    value    = "%[2]s"
+  }
+  
+  detail_filter {
+    name    = "description"
+    operator = "contains"
+    value    = "reliability"
+  }
+}
+
+# Test numeric range filtering (medium target range)
+data "honeycombio_slos" "numeric_range" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name    = "target_percentage"
+    operator = ">"
+    value    = "99.1"
+  }
+  
+  detail_filter {
+    name    = "target_percentage"
+    operator = "<"
+    value    = "99.7"
+  }
+}
+
+# Test combining different operators
+data "honeycombio_slos" "complex_filter" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name    = "name"
+    operator = "starts-with"
+    value    = "%[2]s"
+  }
+  
+  detail_filter {
+    name    = "name"
+    operator = "not-equals"
+    value    = "%[2]s_low_slo"
+  }
+  
+  detail_filter {
+    name    = "time_period"
+    operator = ">"
+    value    = "1"
+  }
+}
+`, dataset, testPrefix),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					// All SLOs should match the combined names filter
+					resource.TestCheckResourceAttr("data.honeycombio_slos.combined_names", "ids.#", "3"),
+
+					// Only medium target SLO should be in this range
+					resource.TestCheckResourceAttr("data.honeycombio_slos.numeric_range", "ids.#", "1"),
+
+					// High and medium SLOs should match complex filter
+					resource.TestCheckResourceAttr("data.honeycombio_slos.complex_filter", "ids.#", "2"),
+				),
+			},
+		},
+	})
+}
+
+// Test for SLO filtering by tags
+func TestAcc_SLOsDataSource_TagFiltering(t *testing.T) {
+	ctx := context.Background()
+	c := testAccClient(t)
+	dataset := testAccDataset()
+	testPrefix := acctest.RandString(8)
+
+	testData := []struct {
+		SLI client.DerivedColumn
+		SLO client.SLO
+	}{
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_prod",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_slo_prod",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_prod"},
+				TimePeriodDays:   30,
+				TargetPerMillion: 999000,
+				Tags: []client.Tag{
+					{Key: "env", Value: "production"},
+					{Key: "level", Value: "critical"},
+					{Key: "team", Value: "core"},
+				},
+			},
+		},
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_staging",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_slo_staging",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_staging"},
+				TimePeriodDays:   30,
+				TargetPerMillion: 995000,
+				Tags: []client.Tag{
+					{Key: "env", Value: "staging"},
+					{Key: "team", Value: "core"},
+				},
+			},
+		},
+		{
+			SLI: client.DerivedColumn{
+				Alias:      testPrefix + "_sli_dev",
+				Expression: "BOOL(1)",
+			},
+			SLO: client.SLO{
+				Name:             testPrefix + "_slo_dev",
+				SLI:              client.SLIRef{Alias: testPrefix + "_sli_dev"},
+				TimePeriodDays:   30,
+				TargetPerMillion: 990000,
+				Tags: []client.Tag{
+					{Key: "env", Value: "development"},
+					{Key: "team", Value: "platform"},
+				},
+			},
+		},
+	}
+
+	for i, tc := range testData {
+		sli, err := c.DerivedColumns.Create(ctx, dataset, &tc.SLI)
+		require.NoError(t, err)
+		slo, err := c.SLOs.Create(ctx, dataset, &tc.SLO)
+		require.NoError(t, err)
+
+		// update IDs for removal later
+		testData[i].SLI.ID = sli.ID
+		testData[i].SLO.ID = slo.ID
+	}
+
+	t.Cleanup(func() {
+		// remove SLOs at the of the test run
+		for _, tc := range testData {
+			c.SLOs.Delete(ctx, dataset, tc.SLO.ID)
+			c.DerivedColumns.Delete(ctx, dataset, tc.SLI.ID)
+		}
+	})
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5ProviderFactory,
+		Steps: []resource.TestStep{
+			{
+				Config: fmt.Sprintf(`
+# Filter SLOs by production tag
+data "honeycombio_slos" "production_slos" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name     = "tags"
+    operator = "contains"
+    value    = "production"
+  }
+}
+
+# Filter SLOs by team:core tag
+data "honeycombio_slos" "core_team_slos" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name     = "tags"
+    operator = "contains"
+    value    = "team:core"
+  }
+}
+
+# Filter SLOs by team:platform tag
+data "honeycombio_slos" "platform_team_slos" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name     = "tags"
+    operator = "contains"
+    value    = "team:platform"
+  }
+}
+
+# Filter SLOs that don't have the development tag
+data "honeycombio_slos" "non_dev_slos" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name     = "tags"
+    operator = "does-not-contain"
+    value    = "development"
+  }
+}
+
+# Filter SLOs with combined tag and name filters
+data "honeycombio_slos" "core_team_with_staging" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name     = "tags"
+    operator = "contains"
+    value    = "team:core"
+  }
+  
+  detail_filter {
+    name     = "name"
+    operator = "contains"
+    value    = "staging"
+  }
+}
+
+# Filter with regex on tags
+data "honeycombio_slos" "team_regex_slos" {
+  dataset = "%[1]s"
+
+  detail_filter {
+    name        = "tags"
+    value_regex = "team:.*"
+  }
+}
+`, dataset, testPrefix),
+				Check: resource.ComposeAggregateTestCheckFunc(
+					resource.TestCheckResourceAttr("data.honeycombio_slos.production_slos", "ids.#", "1"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.core_team_slos", "ids.#", "2"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.platform_team_slos", "ids.#", "1"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.non_dev_slos", "ids.#", "2"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.core_team_with_staging", "ids.#", "1"),
+					resource.TestCheckResourceAttr("data.honeycombio_slos.team_regex_slos", "ids.#", "3"),
+				),
+			},
+		},
+	})
 }