Repository: googleforgames/open-match (go)

Issue:
Allow excluding min/max in DoubleRangeFilter
**Is your feature request related to a problem? Please describe.**
We have a need to subdivide a continuous variable into ranges that are even and predicable, say, 0.0 - 4.0 into 4 buckets:
`[0.0, 1.0)  [1.0, 2.0), [2.0, 3.0), [3.0, 4.0)`
Each bucket with these boundaries would have no overlap. The current inclusive/inclusive boundaries make this very difficult to do without trying to hand make the boundaries to be hopefully close enough but not equal to the next value that there will not be another value in between the ending of one bucket and the start of the next one ie:
`[0.0, 0.99999999...], [1, 1.99999999...]`

We also have used this type of representation for discrete values as well (like enumerations) where it is even more important that the buckets do not overlap.

**Describe the solution you'd like**
We propose no default changes to how things work, by default a query would still run with min/max as inclusive boundaries. 

However, we propose adding a new field to the DoubleRangeFilter contract that introduces the idea that either or both bounds could be exclusive:
```
message DoubleRangeFilter {
  // Name of the ticket's search_fields.double_args this Filter operates on.
  string double_arg = 1;

  // Maximum value.
  double max = 2;

  // Minimum value.
  double min = 3;

  enum Exclude {
    NONE = 0;
    MIN = 1;
    MAX = 2;
    BOTH = 3;
  }
  Exclude exclude = 4;
}
```
This will enable all of the scenarios we currently use and more

**Describe alternatives you've considered**
An alternative to achieve this is attempt to understand the minimum value between the discrete bucket ranges and do some math that will hopefully be close enough to the next buckets minimum range that there is no possibility of a value existing between it. The main concern here is that the computation of these ranges may be done on different platform architectures and may result in slightly varying results at high precision:
So instead of the desired list of filters for a few non overlapping queries:
`[0.0, 1.0)  [1.0, 2.0), [2.0, 3.0), [3.0, 4.0)`
We could try:
`[0.0, 0.99999999...)  [1.0, 1.9999999...), [2.0, 2.9999999...), [3.0, 3.99999999...)`

This has the potential to miss some values between the max of one range and the next range, in addition to being cumbersome to compute and look at compared to the mathematically concise way of defining these.

**Additional context**
We currently run a matchmaker that we would like to port over to using open match as the ticket backend; our existing logic for generating queries that is exposed to customers has the behavior we describe, and it would be a breaking change we would like to avoid to have to convey to customers that they will now need to simulate ranges like this.


Pull request:
Adding Exclude property to DoubleRangeFilter and test coverage.
<!--  Thanks for sending a pull request!  Here are some tips for you:
If this is your first time, please read our contributor guidelines: https://github.com/googleforgames/open-match/blob/master/CONTRIBUTING.md and developer guide https://github.com/googleforgames/open-match/blob/master/docs/development.md
-->

**What this PR does / Why we need it**:
This set of changes gives more control over the excluded/included bounds of DoubleRangeFilters.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Closes #<issue number>`, or `Closes (paste link of issue)`.
-->
Closes #1260

**Special notes for your reviewer**:

Patch:
diff --git a/api/backend.swagger.json b/api/backend.swagger.json
index eb3efa03..f1daac21 100644
--- a/api/backend.swagger.json
+++ b/api/backend.swagger.json
@@ -172,6 +172,17 @@
       ],
       "default": "UNKNOWN"
     },
+    "DoubleRangeFilterExclude": {
+      "type": "string",
+      "enum": [
+        "NONE",
+        "MIN",
+        "MAX",
+        "BOTH"
+      ],
+      "default": "NONE",
+      "title": "- NONE: No bounds should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c= MAX\n - MIN: Only the minimum bound should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c= MAX\n - MAX: Only the maximum bound should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c MAX\n - BOTH: Both bounds should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c MAX"
+    },
     "openmatchAssignTicketsRequest": {
       "type": "object",
       "properties": {
@@ -270,7 +281,8 @@
           "format": "int64",
           "description": "Generation gets incremented on GameServers update operations.\nPrevents the MMF from overriding a newer version from the game server.\nIt is not to be updated by the MMF."
         }
-      }
+      },
+      "description": "BETA FEATURE WARNING:  This call and the associated Request and Response\nmessages are not finalized and still subject to possible change or removal.\nRepresents a backfill entity which is used to fill partially full matches."
     },
     "openmatchDoubleRangeFilter": {
       "type": "object",
@@ -288,6 +300,10 @@
           "type": "number",
           "format": "double",
           "description": "Minimum value."
+        },
+        "exclude": {
+          "$ref": "#/definitions/DoubleRangeFilterExclude",
+          "description": "Which bounds would be excluded when comparing with a ticket's search_fields.double_args value.\n\nBETA FEATURE WARNING: This field and the associated values are\nnot finalized and still subject to possible change or removal."
         }
       },
       "title": "Filters numerical values to only those within a range.\n  double_arg: \"foo\"\n  max: 10\n  min: 5\nmatches:\n  {\"foo\": 5}\n  {\"foo\": 7.5}\n  {\"foo\": 10}\ndoes not match:\n  {\"foo\": 4}\n  {\"foo\": 10.01}\n  {\"foo\": \"7.5\"}\n  {}"
@@ -369,12 +385,12 @@
         },
         "backfill": {
           "$ref": "#/definitions/openmatchBackfill",
-          "title": "Backfill request which contains additional information to the match\nand contains an association to a GameServer"
+          "description": "Backfill request which contains additional information to the match\nand contains an association to a GameServer.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         },
         "allocate_gameserver": {
           "type": "boolean",
           "format": "boolean",
-          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned."
+          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         }
       },
       "description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least\none ticket to be considered as valid."
diff --git a/api/evaluator.swagger.json b/api/evaluator.swagger.json
index 1f4d91f1..c6c875cf 100644
--- a/api/evaluator.swagger.json
+++ b/api/evaluator.swagger.json
@@ -106,7 +106,8 @@
           "format": "int64",
           "description": "Generation gets incremented on GameServers update operations.\nPrevents the MMF from overriding a newer version from the game server.\nIt is not to be updated by the MMF."
         }
-      }
+      },
+      "description": "BETA FEATURE WARNING:  This call and the associated Request and Response\nmessages are not finalized and still subject to possible change or removal.\nRepresents a backfill entity which is used to fill partially full matches."
     },
     "openmatchEvaluateRequest": {
       "type": "object",
@@ -157,12 +158,12 @@
         },
         "backfill": {
           "$ref": "#/definitions/openmatchBackfill",
-          "title": "Backfill request which contains additional information to the match\nand contains an association to a GameServer"
+          "description": "Backfill request which contains additional information to the match\nand contains an association to a GameServer.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         },
         "allocate_gameserver": {
           "type": "boolean",
           "format": "boolean",
-          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned."
+          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         }
       },
       "description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least\none ticket to be considered as valid."
diff --git a/api/frontend.swagger.json b/api/frontend.swagger.json
index 0db1405a..ff8d189f 100644
--- a/api/frontend.swagger.json
+++ b/api/frontend.swagger.json
@@ -385,7 +385,8 @@
           "format": "int64",
           "description": "Generation gets incremented on GameServers update operations.\nPrevents the MMF from overriding a newer version from the game server.\nIt is not to be updated by the MMF."
         }
-      }
+      },
+      "description": "BETA FEATURE WARNING:  This call and the associated Request and Response\nmessages are not finalized and still subject to possible change or removal.\nRepresents a backfill entity which is used to fill partially full matches."
     },
     "openmatchCreateBackfillRequest": {
       "type": "object",
diff --git a/api/matchfunction.swagger.json b/api/matchfunction.swagger.json
index c8a4b05b..bd5a33b4 100644
--- a/api/matchfunction.swagger.json
+++ b/api/matchfunction.swagger.json
@@ -60,6 +60,17 @@
     }
   },
   "definitions": {
+    "DoubleRangeFilterExclude": {
+      "type": "string",
+      "enum": [
+        "NONE",
+        "MIN",
+        "MAX",
+        "BOTH"
+      ],
+      "default": "NONE",
+      "title": "- NONE: No bounds should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c= MAX\n - MIN: Only the minimum bound should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c= MAX\n - MAX: Only the maximum bound should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c MAX\n - BOTH: Both bounds should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c MAX"
+    },
     "openmatchAssignment": {
       "type": "object",
       "properties": {
@@ -105,7 +116,8 @@
           "format": "int64",
           "description": "Generation gets incremented on GameServers update operations.\nPrevents the MMF from overriding a newer version from the game server.\nIt is not to be updated by the MMF."
         }
-      }
+      },
+      "description": "BETA FEATURE WARNING:  This call and the associated Request and Response\nmessages are not finalized and still subject to possible change or removal.\nRepresents a backfill entity which is used to fill partially full matches."
     },
     "openmatchDoubleRangeFilter": {
       "type": "object",
@@ -123,6 +135,10 @@
           "type": "number",
           "format": "double",
           "description": "Minimum value."
+        },
+        "exclude": {
+          "$ref": "#/definitions/DoubleRangeFilterExclude",
+          "description": "Which bounds would be excluded when comparing with a ticket's search_fields.double_args value.\n\nBETA FEATURE WARNING: This field and the associated values are\nnot finalized and still subject to possible change or removal."
         }
       },
       "title": "Filters numerical values to only those within a range.\n  double_arg: \"foo\"\n  max: 10\n  min: 5\nmatches:\n  {\"foo\": 5}\n  {\"foo\": 7.5}\n  {\"foo\": 10}\ndoes not match:\n  {\"foo\": 4}\n  {\"foo\": 10.01}\n  {\"foo\": \"7.5\"}\n  {}"
@@ -158,12 +174,12 @@
         },
         "backfill": {
           "$ref": "#/definitions/openmatchBackfill",
-          "title": "Backfill request which contains additional information to the match\nand contains an association to a GameServer"
+          "description": "Backfill request which contains additional information to the match\nand contains an association to a GameServer.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         },
         "allocate_gameserver": {
           "type": "boolean",
           "format": "boolean",
-          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned."
+          "description": "AllocateGameServer signalise Director that Backfill is new and it should \nallocate a GameServer, this Backfill would be assigned.\n\nBETA FEATURE WARNING: This field is not finalized and still subject\nto possible change or removal."
         }
       },
       "description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least\none ticket to be considered as valid."
diff --git a/api/messages.proto b/api/messages.proto
index bc6c1147..79083e85 100644
--- a/api/messages.proto
+++ b/api/messages.proto
@@ -103,6 +103,26 @@ message DoubleRangeFilter {
 
   // Minimum value.
   double min = 3;
+
+  enum Exclude {
+    // No bounds should be excluded when evaluating the filter, i.e.: MIN <= x <= MAX
+    NONE = 0;
+
+    // Only the minimum bound should be excluded when evaluating the filter, i.e.: MIN < x <= MAX
+    MIN = 1;
+
+    // Only the maximum bound should be excluded when evaluating the filter, i.e.: MIN <= x < MAX
+    MAX = 2;
+
+    // Both bounds should be excluded when evaluating the filter, i.e.: MIN < x < MAX
+    BOTH = 3;
+  }
+
+  // Which bounds would be excluded when comparing with a ticket's search_fields.double_args value.
+  //
+  // BETA FEATURE WARNING: This field and the associated values are
+  // not finalized and still subject to possible change or removal.
+  Exclude exclude = 4;
 }
 
 // Filters strings exactly equaling a value.
diff --git a/api/query.swagger.json b/api/query.swagger.json
index 38534f5e..79960cca 100644
--- a/api/query.swagger.json
+++ b/api/query.swagger.json
@@ -129,6 +129,17 @@
     }
   },
   "definitions": {
+    "DoubleRangeFilterExclude": {
+      "type": "string",
+      "enum": [
+        "NONE",
+        "MIN",
+        "MAX",
+        "BOTH"
+      ],
+      "default": "NONE",
+      "title": "- NONE: No bounds should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c= MAX\n - MIN: Only the minimum bound should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c= MAX\n - MAX: Only the maximum bound should be excluded when evaluating the filter, i.e.: MIN \u003c= x \u003c MAX\n - BOTH: Both bounds should be excluded when evaluating the filter, i.e.: MIN \u003c x \u003c MAX"
+    },
     "openmatchAssignment": {
       "type": "object",
       "properties": {
@@ -193,6 +204,10 @@
           "type": "number",
           "format": "double",
           "description": "Minimum value."
+        },
+        "exclude": {
+          "$ref": "#/definitions/DoubleRangeFilterExclude",
+          "description": "Which bounds would be excluded when comparing with a ticket's search_fields.double_args value.\n\nBETA FEATURE WARNING: This field and the associated values are\nnot finalized and still subject to possible change or removal."
         }
       },
       "title": "Filters numerical values to only those within a range.\n  double_arg: \"foo\"\n  max: 10\n  min: 5\nmatches:\n  {\"foo\": 5}\n  {\"foo\": 7.5}\n  {\"foo\": 10}\ndoes not match:\n  {\"foo\": 4}\n  {\"foo\": 10.01}\n  {\"foo\": \"7.5\"}\n  {}"
diff --git a/internal/filter/filter.go b/internal/filter/filter.go
index 8370c49d..8bbd8f58 100644
--- a/internal/filter/filter.go
+++ b/internal/filter/filter.go
@@ -106,10 +106,27 @@ func (pf *PoolFilter) In(ticket *pb.Ticket) bool {
 		if !ok {
 			return false
 		}
-		// Not simplified so that NaN cases are handled correctly.
-		if !(v >= f.Min && v <= f.Max) {
-			return false
+
+		switch f.Exclude {
+		case pb.DoubleRangeFilter_NONE:
+			// Not simplified so that NaN cases are handled correctly.
+			if !(v >= f.Min && v <= f.Max) {
+				return false
+			}
+		case pb.DoubleRangeFilter_MIN:
+			if !(v > f.Min && v <= f.Max) {
+				return false
+			}
+		case pb.DoubleRangeFilter_MAX:
+			if !(v >= f.Min && v < f.Max) {
+				return false
+			}
+		case pb.DoubleRangeFilter_BOTH:
+			if !(v > f.Min && v < f.Max) {
+				return false
+			}
 		}
+
 	}
 
 	for _, f := range pf.StringEqualsFilters {
diff --git a/pkg/pb/messages.pb.go b/pkg/pb/messages.pb.go
index fffa8059..3dcf5cb4 100644
--- a/pkg/pb/messages.pb.go
+++ b/pkg/pb/messages.pb.go
@@ -23,6 +23,41 @@ var _ = math.Inf
 // proto package needs to be updated.
 const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
 
+type DoubleRangeFilter_Exclude int32
+
+const (
+	// No bounds should be excluded when evaluating the filter, i.e.: MIN <= x <= MAX
+	DoubleRangeFilter_NONE DoubleRangeFilter_Exclude = 0
+	// Only the minimum bound should be excluded when evaluating the filter, i.e.: MIN < x <= MAX
+	DoubleRangeFilter_MIN DoubleRangeFilter_Exclude = 1
+	// Only the maximum bound should be excluded when evaluating the filter, i.e.: MIN <= x < MAX
+	DoubleRangeFilter_MAX DoubleRangeFilter_Exclude = 2
+	// Both bounds should be excluded when evaluating the filter, i.e.: MIN < x < MAX
+	DoubleRangeFilter_BOTH DoubleRangeFilter_Exclude = 3
+)
+
+var DoubleRangeFilter_Exclude_name = map[int32]string{
+	0: "NONE",
+	1: "MIN",
+	2: "MAX",
+	3: "BOTH",
+}
+
+var DoubleRangeFilter_Exclude_value = map[string]int32{
+	"NONE": 0,
+	"MIN":  1,
+	"MAX":  2,
+	"BOTH": 3,
+}
+
+func (x DoubleRangeFilter_Exclude) String() string {
+	return proto.EnumName(DoubleRangeFilter_Exclude_name, int32(x))
+}
+
+func (DoubleRangeFilter_Exclude) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_cb9fb1f207fd5b8c, []int{3, 0}
+}
+
 // A Ticket is a basic matchmaking entity in Open Match. A Ticket may represent
 // an individual 'Player', a 'Group' of players, or any other concepts unique to
 // your use case. Open Match will not interpret what the Ticket represents but
@@ -243,10 +278,15 @@ type DoubleRangeFilter struct {
 	// Maximum value.
 	Max float64 `protobuf:"fixed64,2,opt,name=max,proto3" json:"max,omitempty"`
 	// Minimum value.
-	Min                  float64  `protobuf:"fixed64,3,opt,name=min,proto3" json:"min,omitempty"`
-	XXX_NoUnkeyedLiteral struct{} `json:"-"`
-	XXX_unrecognized     []byte   `json:"-"`
-	XXX_sizecache        int32    `json:"-"`
+	Min float64 `protobuf:"fixed64,3,opt,name=min,proto3" json:"min,omitempty"`
+	// Which bounds would be excluded when comparing with a ticket's search_fields.double_args value.
+	//
+	// BETA FEATURE WARNING: This field and the associated values are
+	// not finalized and still subject to possible change or removal.
+	Exclude              DoubleRangeFilter_Exclude `protobuf:"varint,4,opt,name=exclude,proto3,enum=openmatch.DoubleRangeFilter_Exclude" json:"exclude,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}                  `json:"-"`
+	XXX_unrecognized     []byte                    `json:"-"`
+	XXX_sizecache        int32                     `json:"-"`
 }
 
 func (m *DoubleRangeFilter) Reset()         { *m = DoubleRangeFilter{} }
@@ -295,6 +335,13 @@ func (m *DoubleRangeFilter) GetMin() float64 {
 	return 0
 }
 
+func (m *DoubleRangeFilter) GetExclude() DoubleRangeFilter_Exclude {
+	if m != nil {
+		return m.Exclude
+	}
+	return DoubleRangeFilter_NONE
+}
+
 // Filters strings exactly equaling a value.
 //   string_arg: "foo"
 //   value: "bar"
@@ -571,14 +618,14 @@ type Match struct {
 	// Backfill request which contains additional information to the match
 	// and contains an association to a GameServer.
 	//
-	// BETA FEATURE WARNING:  This call and the associated Request and Response
-	// messages are not finalized and still subject to possible change or removal.
+	// BETA FEATURE WARNING: This field is not finalized and still subject
+	// to possible change or removal.
 	Backfill *Backfill `protobuf:"bytes,8,opt,name=backfill,proto3" json:"backfill,omitempty"`
 	// AllocateGameServer signalise Director that Backfill is new and it should
 	// allocate a GameServer, this Backfill would be assigned.
 	//
-	// BETA FEATURE WARNING:  This call and the associated Request and Response
-	// messages are not finalized and still subject to possible change or removal.
+	// BETA FEATURE WARNING: This field is not finalized and still subject
+	// to possible change or removal.
 	AllocateGameserver   bool     `protobuf:"varint,9,opt,name=allocate_gameserver,json=allocateGameserver,proto3" json:"allocate_gameserver,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
@@ -745,6 +792,7 @@ func (m *Backfill) GetGeneration() int64 {
 }
 
 func init() {
+	proto.RegisterEnum("openmatch.DoubleRangeFilter_Exclude", DoubleRangeFilter_Exclude_name, DoubleRangeFilter_Exclude_value)
 	proto.RegisterType((*Ticket)(nil), "openmatch.Ticket")
 	proto.RegisterMapType((map[string]*any.Any)(nil), "openmatch.Ticket.ExtensionsEntry")
 	proto.RegisterType((*SearchFields)(nil), "openmatch.SearchFields")
@@ -767,64 +815,67 @@ func init() {
 func init() { proto.RegisterFile("api/messages.proto", fileDescriptor_cb9fb1f207fd5b8c) }
 
 var fileDescriptor_cb9fb1f207fd5b8c = []byte{
-	// 933 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x56, 0xdd, 0x6e, 0xe3, 0x44,
-	0x14, 0x96, 0x7f, 0xf2, 0x77, 0x92, 0xb6, 0xe9, 0xb4, 0xab, 0xf5, 0x06, 0x16, 0x42, 0x96, 0x8a,
-	0x08, 0x84, 0x2d, 0x15, 0x21, 0x21, 0x7e, 0x04, 0x29, 0xb4, 0xcb, 0x16, 0x01, 0xc5, 0xad, 0xb8,
-	0xe0, 0x26, 0x9a, 0xd8, 0x13, 0xaf, 0x55, 0x7b, 0x6c, 0x3c, 0x93, 0x6a, 0xfb, 0x1e, 0x3c, 0x00,
-	0xd7, 0x5c, 0xf3, 0x1e, 0xdc, 0xf0, 0x10, 0xdc, 0xf3, 0x02, 0x68, 0x66, 0x6c, 0x67, 0x9a, 0x84,
-	0x96, 0x5e, 0x54, 0xdc, 0x8d, 0xcf, 0xcf, 0x37, 0xe7, 0x7c, 0xf3, 0xf9, 0xcc, 0x00, 0xc2, 0x79,
-	0xec, 0xa5, 0x84, 0x31, 0x1c, 0x11, 0xe6, 0xe6, 0x45, 0xc6, 0x33, 0xd4, 0xc9, 0x72, 0x42, 0x53,
-	0xcc, 0x83, 0x97, 0x83, 0xc7, 0x51, 0x96, 0x45, 0x09, 0xf1, 0x8a, 0x3c, 0xf0, 0x18, 0xc7, 0x7c,
-	0x51, 0xc6, 0x0c, 0x9e, 0x94, 0x0e, 0xf9, 0x35, 0x5b, 0xcc, 0x3d, 0x4c, 0xaf, 0x4b, 0xd7, 0x9b,
-	0xab, 0x2e, 0x1e, 0xa7, 0x84, 0x71, 0x9c, 0xe6, 0x2a, 0x60, 0xf4, 0x97, 0x09, 0xcd, 0x8b, 0x38,
-	0xb8, 0x24, 0x1c, 0x6d, 0x83, 0x19, 0x87, 0x8e, 0x31, 0x34, 0xc6, 0x1d, 0xdf, 0x8c, 0x43, 0xf4,
-	0x21, 0x00, 0x66, 0x2c, 0x8e, 0x68, 0x4a, 0x28, 0x77, 0xac, 0xa1, 0x31, 0xee, 0x1e, 0x3e, 0x72,
-	0xeb, 0x7a, 0xdc, 0x49, 0xed, 0xf4, 0xb5, 0x40, 0xf4, 0x29, 0x6c, 0x31, 0x82, 0x8b, 0xe0, 0xe5,
-	0x74, 0x1e, 0x93, 0x24, 0x64, 0x8e, 0x2d, 0x33, 0x1f, 0x6b, 0x99, 0xe7, 0xd2, 0x7f, 0x22, 0xdd,
-	0x7e, 0x8f, 0x69, 0x5f, 0x68, 0x02, 0x40, 0x5e, 0x71, 0x42, 0x59, 0x9c, 0x51, 0xe6, 0x34, 0x86,
-	0xd6, 0xb8, 0x7b, 0xf8, 0x96, 0x96, 0xaa, 0x6a, 0x75, 0x8f, 0xeb, 0x98, 0x63, 0xca, 0x8b, 0x6b,
-	0x5f, 0x4b, 0x42, 0x9f, 0x40, 0x37, 0x28, 0x08, 0xe6, 0x64, 0x2a, 0x9a, 0x75, 0x9a, 0x72, 0xfb,
-	0x81, 0xab, 0x98, 0x70, 0x2b, 0x26, 0xdc, 0x8b, 0x8a, 0x09, 0x1f, 0x54, 0xb8, 0x30, 0x0c, 0xce,
-	0x61, 0x67, 0x05, 0x1b, 0xf5, 0xc1, 0xba, 0x24, 0xd7, 0x25, 0x31, 0x62, 0x89, 0xde, 0x85, 0xc6,
-	0x15, 0x4e, 0x16, 0xc4, 0x31, 0x25, 0xf6, 0xfe, 0x1a, 0xf6, 0x84, 0x5e, 0xfb, 0x2a, 0xe4, 0x63,
-	0xf3, 0x23, 0xe3, 0xd4, 0x6e, 0x9b, 0x7d, 0x6b, 0xf4, 0xbb, 0x09, 0x3d, 0xbd, 0x73, 0xf4, 0x35,
-	0x74, 0xc3, 0x6c, 0x31, 0x4b, 0xc8, 0x14, 0x17, 0x11, 0x73, 0x0c, 0xd9, 0xec, 0x3b, 0xff, 0xc2,
-	0x93, 0xfb, 0x95, 0x0c, 0x9d, 0x14, 0x51, 0xd5, 0x72, 0x58, 0x1b, 0x04, 0x12, 0xe3, 0x45, 0x4c,
-	0x23, 0x85, 0x64, 0xde, 0x8e, 0x74, 0x2e, 0x43, 0x35, 0x24, 0x56, 0x1b, 0x10, 0x02, 0x9b, 0xe3,
-	0x88, 0x39, 0xd6, 0xd0, 0x1a, 0x77, 0x7c, 0xb9, 0x1e, 0x7c, 0x06, 0x3b, 0x2b, 0x9b, 0x6f, 0xe0,
-	0x64, 0x5f, 0xe7, 0xc4, 0xd0, 0xba, 0x17, 0xe9, 0x2b, 0x3b, 0xde, 0x95, 0xde, 0xd1, 0xd2, 0x47,
-	0x7f, 0x1a, 0x00, 0x4b, 0xa9, 0xa1, 0x37, 0x00, 0x82, 0x8c, 0x52, 0x12, 0xf0, 0x38, 0xa3, 0x25,
-	0x82, 0x66, 0x41, 0xc7, 0x37, 0x04, 0x64, 0x4b, 0x26, 0x0e, 0x36, 0xaa, 0xf6, 0x36, 0x11, 0x3d,
-	0xa0, 0x0e, 0x4e, 0xed, 0xb6, 0xd5, 0xb7, 0x47, 0x3f, 0xc2, 0xae, 0x22, 0xd5, 0xc7, 0x34, 0x22,
-	0x27, 0x71, 0xc2, 0x49, 0x81, 0x9e, 0x02, 0x2c, 0x15, 0x51, 0xee, 0xd4, 0xa9, 0xcf, 0x59, 0x54,
-	0x90, 0xe2, 0x57, 0x25, 0xc3, 0x62, 0x29, 0x2d, 0x31, 0x95, 0x3f, 0xa7, 0xb0, 0xc4, 0x74, 0xf4,
-	0x02, 0x90, 0x62, 0xfb, 0xf8, 0xe7, 0x05, 0x4e, 0xd8, 0x12, 0x78, 0x29, 0x90, 0x0a, 0xb8, 0x3e,
-	0xf6, 0xcd, 0xec, 0x8f, 0xde, 0x86, 0xfe, 0x05, 0x8e, 0xce, 0x0a, 0xc2, 0x08, 0xe5, 0x25, 0x50,
-	0x1f, 0x2c, 0x8e, 0x2b, 0x04, 0xb1, 0x1c, 0xfd, 0x62, 0x81, 0x7d, 0x96, 0x65, 0x89, 0x90, 0x0e,
-	0xc5, 0x29, 0x29, 0x7d, 0x72, 0x8d, 0xbe, 0x83, 0xfd, 0xb2, 0xa1, 0x42, 0xb4, 0x39, 0x9d, 0x4b,
-	0x94, 0x4a, 0xa1, 0xaf, 0x6b, 0xe7, 0xb2, 0x46, 0x86, 0x8f, 0xc2, 0x55, 0x13, 0x43, 0x3f, 0xc0,
-	0xa3, 0xb2, 0x0f, 0x22, 0xdb, 0xab, 0x01, 0xd5, 0x41, 0x3f, 0xd5, 0x25, 0xbf, 0xc6, 0x82, 0xbf,
-	0xc7, 0xd6, 0x6c, 0x0c, 0x7d, 0x03, 0x7b, 0x1c, 0x47, 0xd3, 0x5c, 0xb5, 0x59, 0x03, 0xaa, 0xd1,
-	0xf3, 0x9a, 0x3e, 0x7a, 0x56, 0xb8, 0xf0, 0x77, 0xf9, 0x8a, 0x45, 0x8c, 0xaf, 0x6d, 0x35, 0x4c,
-	0xc2, 0xe9, 0x8c, 0xcc, 0xb3, 0xe2, 0xbf, 0x8c, 0x9f, 0xad, 0x32, 0xe3, 0x48, 0x26, 0xa0, 0xcf,
-	0xa1, 0x32, 0x4c, 0xf1, 0x9c, 0x93, 0xc2, 0x69, 0xdd, 0x89, 0xd0, 0x2b, 0x13, 0x26, 0x22, 0xbe,
-	0xd4, 0xd7, 0xdf, 0x06, 0xf4, 0xbe, 0x15, 0x75, 0x9f, 0x15, 0xd9, 0x3c, 0x4e, 0xc8, 0xc6, 0xe3,
-	0x39, 0x80, 0x46, 0x9e, 0x65, 0x89, 0xfa, 0xdd, 0xbb, 0x87, 0x3b, 0x5a, 0xb7, 0xe2, 0x48, 0x7d,
-	0xe5, 0x45, 0xcf, 0x37, 0x0c, 0x65, 0x7d, 0xba, 0xe8, 0xfb, 0xfc, 0x7f, 0x7f, 0x95, 0xdd, 0x6f,
-	0x8c, 0x7e, 0xb5, 0xa0, 0x21, 0xab, 0x41, 0x4f, 0xa0, 0x2d, 0x8b, 0x9b, 0xd6, 0x77, 0x5a, 0x4b,
-	0x7e, 0xbf, 0x08, 0xd1, 0x33, 0xd8, 0x52, 0xae, 0x5c, 0x95, 0x5c, 0xaa, 0xbe, 0x97, 0xea, 0x74,
-	0x1d, 0xc0, 0xb6, 0x0a, 0x9a, 0x2f, 0xa8, 0x9a, 0x35, 0x96, 0x8c, 0x52, 0xa9, 0x27, 0xa5, 0x11,
-	0xbd, 0x07, 0x2d, 0x2e, 0xaf, 0xa4, 0x4a, 0x82, 0xbb, 0x6b, 0x97, 0x95, 0x5f, 0x45, 0xa0, 0x2f,
-	0x6e, 0xf0, 0xd8, 0x92, 0xf1, 0xc3, 0x55, 0x1e, 0x6f, 0xbd, 0xdb, 0x3c, 0x68, 0xcf, 0x70, 0x70,
-	0x39, 0x8f, 0x93, 0xc4, 0x69, 0x4b, 0x7a, 0xf6, 0xb4, 0xfc, 0xa3, 0xd2, 0xe5, 0xd7, 0x41, 0xc8,
-	0x83, 0x3d, 0x9c, 0x24, 0x59, 0x20, 0xae, 0xc3, 0x08, 0xa7, 0x84, 0x91, 0xe2, 0x8a, 0x14, 0x4e,
-	0x67, 0x68, 0x8c, 0xdb, 0x3e, 0xaa, 0x5c, 0xcf, 0x6b, 0xcf, 0x43, 0x1d, 0x51, 0xa3, 0xdf, 0x3c,
-	0xb5, 0xdb, 0xcd, 0x7e, 0x6b, 0xf4, 0x87, 0x09, 0xed, 0xaa, 0xd0, 0xb5, 0x37, 0xc7, 0xda, 0xe3,
-	0xc1, 0xbc, 0xcf, 0xe3, 0xe1, 0xcb, 0x1b, 0xfc, 0x2a, 0x4d, 0x3f, 0xdb, 0xc0, 0xcf, 0x7d, 0x9e,
-	0x0f, 0xf6, 0x7d, 0x9e, 0x0f, 0xe2, 0x76, 0x8a, 0x08, 0x25, 0x05, 0x96, 0x8a, 0x69, 0x0c, 0x8d,
-	0xb1, 0xe5, 0x6b, 0x96, 0x07, 0x61, 0xf7, 0xc8, 0xfd, 0x69, 0x28, 0x7a, 0x7c, 0x5f, 0x35, 0x19,
-	0x92, 0x2b, 0x6f, 0xf9, 0xe9, 0xe5, 0x97, 0x91, 0x97, 0xcf, 0x7e, 0x33, 0x3b, 0xdf, 0xe7, 0x84,
-	0x4a, 0x81, 0xcd, 0x9a, 0x12, 0xe8, 0x83, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x5e, 0x41, 0xdd,
-	0x4c, 0x70, 0x0a, 0x00, 0x00,
+	// 992 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x56, 0x4d, 0x73, 0xe3, 0x34,
+	0x18, 0xc6, 0x1f, 0x69, 0x92, 0xb7, 0xd9, 0xd6, 0x55, 0xbb, 0xb3, 0xde, 0xc0, 0x42, 0xf0, 0x6e,
+	0x87, 0x0c, 0x0c, 0xf6, 0x4c, 0x19, 0x66, 0x18, 0xbe, 0x53, 0x48, 0x77, 0x5b, 0x66, 0xdb, 0xe2,
+	0xf6, 0xc0, 0x70, 0xc9, 0x28, 0xb6, 0xe2, 0xf5, 0xd4, 0x91, 0x8d, 0xa5, 0x74, 0xda, 0xff, 0xc1,
+	0x0f, 0xe0, 0xcc, 0x99, 0xff, 0xc0, 0x91, 0x0b, 0x3f, 0x82, 0x3b, 0x7f, 0x80, 0xb1, 0x64, 0x3b,
+	0x6a, 0x12, 0xba, 0xf4, 0xd0, 0xe1, 0x26, 0xbd, 0x5f, 0xd2, 0xf3, 0xe8, 0xd1, 0x2b, 0x01, 0xc2,
+	0x59, 0xec, 0x4d, 0x09, 0x63, 0x38, 0x22, 0xcc, 0xcd, 0xf2, 0x94, 0xa7, 0xa8, 0x9d, 0x66, 0x84,
+	0x4e, 0x31, 0x0f, 0x5e, 0x75, 0x1f, 0x45, 0x69, 0x1a, 0x25, 0xc4, 0xcb, 0xb3, 0xc0, 0x63, 0x1c,
+	0xf3, 0x59, 0x19, 0xd3, 0x7d, 0x5c, 0x3a, 0xc4, 0x6c, 0x3c, 0x9b, 0x78, 0x98, 0x5e, 0x97, 0xae,
+	0x77, 0x16, 0x5d, 0x3c, 0x9e, 0x12, 0xc6, 0xf1, 0x34, 0x93, 0x01, 0xce, 0x5f, 0x3a, 0xac, 0x9d,
+	0xc7, 0xc1, 0x05, 0xe1, 0x68, 0x03, 0xf4, 0x38, 0xb4, 0xb5, 0x9e, 0xd6, 0x6f, 0xfb, 0x7a, 0x1c,
+	0xa2, 0x8f, 0x01, 0x30, 0x63, 0x71, 0x44, 0xa7, 0x84, 0x72, 0xdb, 0xe8, 0x69, 0xfd, 0xf5, 0xbd,
+	0x87, 0x6e, 0xbd, 0x1f, 0x77, 0x50, 0x3b, 0x7d, 0x25, 0x10, 0x7d, 0x0e, 0x0f, 0x18, 0xc1, 0x79,
+	0xf0, 0x6a, 0x34, 0x89, 0x49, 0x12, 0x32, 0xdb, 0x14, 0x99, 0x8f, 0x94, 0xcc, 0x33, 0xe1, 0x3f,
+	0x10, 0x6e, 0xbf, 0xc3, 0x94, 0x19, 0x1a, 0x00, 0x90, 0x2b, 0x4e, 0x28, 0x8b, 0x53, 0xca, 0xec,
+	0x46, 0xcf, 0xe8, 0xaf, 0xef, 0xbd, 0xab, 0xa4, 0xca, 0xbd, 0xba, 0xc3, 0x3a, 0x66, 0x48, 0x79,
+	0x7e, 0xed, 0x2b, 0x49, 0xe8, 0x33, 0x58, 0x0f, 0x72, 0x82, 0x39, 0x19, 0x15, 0x60, 0xed, 0x35,
+	0xb1, 0x7c, 0xd7, 0x95, 0x4c, 0xb8, 0x15, 0x13, 0xee, 0x79, 0xc5, 0x84, 0x0f, 0x32, 0xbc, 0x30,
+	0x74, 0xcf, 0x60, 0x73, 0xa1, 0x36, 0xb2, 0xc0, 0xb8, 0x20, 0xd7, 0x25, 0x31, 0xc5, 0x10, 0xbd,
+	0x0f, 0x8d, 0x4b, 0x9c, 0xcc, 0x88, 0xad, 0x8b, 0xda, 0x3b, 0x4b, 0xb5, 0x07, 0xf4, 0xda, 0x97,
+	0x21, 0x9f, 0xea, 0x9f, 0x68, 0x47, 0x66, 0x4b, 0xb7, 0x0c, 0xe7, 0x37, 0x1d, 0x3a, 0x2a, 0x72,
+	0xf4, 0x02, 0xd6, 0xc3, 0x74, 0x36, 0x4e, 0xc8, 0x08, 0xe7, 0x11, 0xb3, 0x35, 0x01, 0xf6, 0xbd,
+	0x7f, 0xe1, 0xc9, 0xfd, 0x56, 0x84, 0x0e, 0xf2, 0xa8, 0x82, 0x1c, 0xd6, 0x86, 0xa2, 0x12, 0xe3,
+	0x79, 0x4c, 0x23, 0x59, 0x49, 0xbf, 0xbd, 0xd2, 0x99, 0x08, 0x55, 0x2a, 0xb1, 0xda, 0x80, 0x10,
+	0x98, 0x1c, 0x47, 0xcc, 0x36, 0x7a, 0x46, 0xbf, 0xed, 0x8b, 0x71, 0xf7, 0x0b, 0xd8, 0x5c, 0x58,
+	0x7c, 0x05, 0x27, 0x3b, 0x2a, 0x27, 0x9a, 0x82, 0xbe, 0x48, 0x5f, 0x58, 0xf1, 0x75, 0xe9, 0x6d,
+	0x25, 0xdd, 0xf9, 0x53, 0x03, 0x98, 0x4b, 0x0d, 0xbd, 0x0d, 0x10, 0xa4, 0x94, 0x92, 0x80, 0xc7,
+	0x29, 0x2d, 0x2b, 0x28, 0x16, 0x34, 0xbc, 0x21, 0x20, 0x53, 0x30, 0xb1, 0xbb, 0x52, 0xb5, 0xb7,
+	0x89, 0xe8, 0x1e, 0x75, 0x70, 0x64, 0xb6, 0x0c, 0xcb, 0x74, 0x7e, 0xd7, 0x60, 0x4b, 0xb2, 0xea,
+	0x63, 0x1a, 0x91, 0x83, 0x38, 0xe1, 0x24, 0x47, 0x4f, 0x00, 0xe6, 0x92, 0x28, 0x97, 0x6a, 0xd7,
+	0x07, 0x5d, 0x6c, 0x61, 0x8a, 0xaf, 0x4a, 0x8a, 0x8b, 0xa1, 0xb0, 0xc4, 0x54, 0xdc, 0xce, 0xc2,
+	0x12, 0x53, 0xf4, 0x25, 0x34, 0xc9, 0x55, 0x90, 0xcc, 0x42, 0x22, 0x6e, 0xde, 0xc6, 0xde, 0x33,
+	0x05, 0xfd, 0xd2, 0x8a, 0xee, 0x50, 0xc6, 0xfa, 0x55, 0x92, 0xe3, 0x41, 0xb3, 0xb4, 0xa1, 0x16,
+	0x98, 0xc7, 0x27, 0xc7, 0x43, 0xeb, 0x0d, 0xd4, 0x04, 0xe3, 0xe5, 0xe1, 0xb1, 0xa5, 0x89, 0xc1,
+	0xe0, 0x07, 0x4b, 0x2f, 0x7c, 0xfb, 0x27, 0xe7, 0x2f, 0x2c, 0xc3, 0x39, 0x04, 0x24, 0xcf, 0x77,
+	0xf8, 0xd3, 0x0c, 0x27, 0x6c, 0x8e, 0x64, 0x2e, 0xc9, 0x0a, 0x49, 0x2d, 0xb4, 0xd5, 0xe7, 0xed,
+	0x3c, 0x03, 0xeb, 0x1c, 0x47, 0xa7, 0x39, 0x61, 0x84, 0xf2, 0xb2, 0x90, 0x05, 0x06, 0xc7, 0x55,
+	0x85, 0x62, 0xe8, 0xfc, 0x6c, 0x80, 0x79, 0x9a, 0xa6, 0x49, 0x21, 0x56, 0x8a, 0xa7, 0xa4, 0xf4,
+	0x89, 0x31, 0x3a, 0x86, 0x9d, 0x92, 0xc1, 0xbc, 0x40, 0x39, 0x9a, 0x88, 0x2a, 0xd5, 0x9d, 0x78,
+	0xeb, 0x36, 0x2e, 0x7c, 0x14, 0x2e, 0x9a, 0x18, 0xfa, 0x1e, 0x1e, 0x96, 0x38, 0x88, 0x80, 0x57,
+	0x17, 0x94, 0xd2, 0x7a, 0xa2, 0x5e, 0xb2, 0x25, 0x16, 0xfc, 0x6d, 0xb6, 0x64, 0x63, 0xe8, 0x3b,
+	0xd8, 0xe6, 0x38, 0x1a, 0x65, 0x12, 0x66, 0x5d, 0x50, 0x36, 0xbb, 0x37, 0xd5, 0x66, 0xb7, 0xc0,
+	0x85, 0xbf, 0xc5, 0x17, 0x2c, 0x45, 0xc3, 0xdc, 0x90, 0xed, 0x2b, 0x1c, 0x8d, 0xc9, 0x24, 0xcd,
+	0xff, 0x4b, 0xc3, 0x7b, 0x50, 0x66, 0xec, 0x8b, 0x04, 0xf4, 0x15, 0x54, 0x86, 0x11, 0x9e, 0x70,
+	0x92, 0xdb, 0xcd, 0xd7, 0x56, 0xe8, 0x94, 0x09, 0x83, 0x22, 0xbe, 0x54, 0xf4, 0xdf, 0x1a, 0x74,
+	0x5e, 0x16, 0xfb, 0x3e, 0xcd, 0xd3, 0x49, 0x9c, 0x90, 0x95, 0xc7, 0xb3, 0x0b, 0x8d, 0x2c, 0x4d,
+	0x13, 0xd9, 0x60, 0xd6, 0xf7, 0x36, 0x15, 0xb4, 0xc5, 0x91, 0xfa, 0xd2, 0x8b, 0x9e, 0xaf, 0x78,
+	0x06, 0xd4, 0x7e, 0xa6, 0xae, 0xf3, 0xff, 0xdd, 0x63, 0xd3, 0x6a, 0x38, 0xbf, 0x18, 0xd0, 0x10,
+	0xbb, 0x41, 0x8f, 0xa1, 0x25, 0x36, 0x37, 0xaa, 0x5f, 0xd1, 0xa6, 0x98, 0x1f, 0x86, 0xe8, 0x29,
+	0x3c, 0x90, 0xae, 0x4c, 0x6e, 0xb9, 0x54, 0x7d, 0x67, 0xaa, 0xd2, 0xb5, 0x0b, 0x1b, 0x32, 0x68,
+	0x32, 0xa3, 0xb2, 0xbb, 0x19, 0x22, 0x4a, 0xa6, 0x1e, 0x94, 0x46, 0xf4, 0x01, 0x34, 0xb9, 0x78,
+	0x04, 0x2b, 0x09, 0x6e, 0x2d, 0x3d, 0x8f, 0x7e, 0x15, 0x81, 0xbe, 0xbe, 0xc1, 0x63, 0x53, 0xc4,
+	0xf7, 0x16, 0x79, 0xbc, 0xf5, 0x35, 0xf5, 0xa0, 0x35, 0xc6, 0xc1, 0xc5, 0x24, 0x4e, 0x12, 0xbb,
+	0x25, 0xe8, 0xd9, 0x56, 0xf2, 0xf7, 0x4b, 0x97, 0x5f, 0x07, 0x21, 0x0f, 0xb6, 0x71, 0x92, 0xa4,
+	0x41, 0xf1, 0x00, 0x47, 0x78, 0x4a, 0x18, 0xc9, 0x2f, 0x49, 0x6e, 0xb7, 0x7b, 0x5a, 0xbf, 0xe5,
+	0xa3, 0xca, 0xf5, 0xbc, 0xf6, 0xdc, 0xd7, 0x11, 0x35, 0xac, 0xb5, 0x23, 0xb3, 0xb5, 0x66, 0x35,
+	0x9d, 0x3f, 0x74, 0x68, 0x55, 0x1b, 0x5d, 0xfa, 0xe5, 0x2c, 0x7d, 0x57, 0xf4, 0xbb, 0x7c, 0x57,
+	0xbe, 0xb9, 0xc1, 0xaf, 0xd4, 0xf4, 0xd3, 0x15, 0xfc, 0xdc, 0xe5, 0xc3, 0x62, 0xde, 0xe5, 0xc3,
+	0x52, 0xbc, 0x87, 0x11, 0xa1, 0x24, 0xc7, 0x42, 0x31, 0x8d, 0x9e, 0xd6, 0x37, 0x7c, 0xc5, 0x72,
+	0x2f, 0xec, 0xee, 0xbb, 0x3f, 0xf6, 0x0a, 0x8c, 0x1f, 0x4a, 0x90, 0x21, 0xb9, 0xf4, 0xe6, 0x53,
+	0x2f, 0xbb, 0x88, 0xbc, 0x6c, 0xfc, 0xab, 0xde, 0x3e, 0xc9, 0x08, 0x15, 0x02, 0x1b, 0xaf, 0x89,
+	0x42, 0x1f, 0xfd, 0x13, 0x00, 0x00, 0xff, 0xff, 0x0a, 0xaa, 0xfb, 0x6f, 0xe2, 0x0a, 0x00, 0x00,
 }


Test patch:
diff --git a/internal/filter/testcases/testcases.go b/internal/filter/testcases/testcases.go
index 4d180411..1c0f6e9b 100644
--- a/internal/filter/testcases/testcases.go
+++ b/internal/filter/testcases/testcases.go
@@ -43,10 +43,28 @@ func IncludedTestCases() []TestCase {
 			&pb.Pool{},
 		},
 
-		simpleDoubleRange("simpleInRange", 5, 0, 10),
-		simpleDoubleRange("exactMatch", 5, 5, 5),
-		simpleDoubleRange("infinityMax", math.Inf(1), 0, math.Inf(1)),
-		simpleDoubleRange("infinityMin", math.Inf(-1), math.Inf(-1), 0),
+		simpleDoubleRange("simpleInRange", 5, 0, 10, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("simpleInRange", 5, 0, 10, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("simpleInRange", 5, 0, 10, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("simpleInRange", 5, 0, 10, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("exactMatch", 5, 5, 5, pb.DoubleRangeFilter_NONE),
+
+		simpleDoubleRange("infinityMax", math.Inf(1), 0, math.Inf(1), pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("infinityMax", math.Inf(1), 0, math.Inf(1), pb.DoubleRangeFilter_MIN),
+
+		simpleDoubleRange("infinityMin", math.Inf(-1), math.Inf(-1), 0, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("infinityMin", math.Inf(-1), math.Inf(-1), 0, pb.DoubleRangeFilter_MAX),
+
+		simpleDoubleRange("excludeNone", 0, 0, 1, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("excludeNone", 1, 0, 1, pb.DoubleRangeFilter_NONE),
+
+		simpleDoubleRange("excludeMin", 1, 0, 1, pb.DoubleRangeFilter_MIN),
+
+		simpleDoubleRange("excludeMax", 0, 0, 1, pb.DoubleRangeFilter_MAX),
+
+		simpleDoubleRange("excludeBoth", 2, 0, 3, pb.DoubleRangeFilter_BOTH),
+		simpleDoubleRange("excludeBoth", 1, 0, 3, pb.DoubleRangeFilter_BOTH),
 
 		{
 			"String equals simple positive",
@@ -182,7 +200,6 @@ func ExcludedTestCases() []TestCase {
 				},
 			},
 		},
-
 		{
 			"double range missing field",
 			&pb.Ticket{
@@ -203,14 +220,60 @@ func ExcludedTestCases() []TestCase {
 			},
 		},
 
-		simpleDoubleRange("valueTooLow", -1, 0, 10),
-		simpleDoubleRange("valueTooHigh", 11, 0, 10),
-		simpleDoubleRange("minIsNan", 5, math.NaN(), 10),
-		simpleDoubleRange("maxIsNan", 5, 0, math.NaN()),
-		simpleDoubleRange("minMaxAreNan", 5, math.NaN(), math.NaN()),
-		simpleDoubleRange("valueIsNan", math.NaN(), 0, 10),
-		simpleDoubleRange("valueIsNanInfRange", math.NaN(), math.Inf(-1), math.Inf(1)),
-		simpleDoubleRange("allAreNan", math.NaN(), math.NaN(), math.NaN()),
+		simpleDoubleRange("exactMatch", 5, 5, 5, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("exactMatch", 5, 5, 5, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("exactMatch", 5, 5, 5, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("valueTooLow", -1, 0, 10, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("valueTooLow", -1, 0, 10, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("valueTooLow", -1, 0, 10, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("valueTooLow", -1, 0, 10, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("valueTooHigh", 11, 0, 10, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("valueTooHigh", 11, 0, 10, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("valueTooHigh", 11, 0, 10, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("valueTooHigh", 11, 0, 10, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("minIsNan", 5, math.NaN(), 10, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("minIsNan", 5, math.NaN(), 10, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("minIsNan", 5, math.NaN(), 10, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("minIsNan", 5, math.NaN(), 10, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("maxIsNan", 5, 0, math.NaN(), pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("maxIsNan", 5, 0, math.NaN(), pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("maxIsNan", 5, 0, math.NaN(), pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("maxIsNan", 5, 0, math.NaN(), pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("minMaxAreNan", 5, math.NaN(), math.NaN(), pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("minMaxAreNan", 5, math.NaN(), math.NaN(), pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("minMaxAreNan", 5, math.NaN(), math.NaN(), pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("minMaxAreNan", 5, math.NaN(), math.NaN(), pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("valueIsNan", math.NaN(), 0, 10, pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("valueIsNan", math.NaN(), 0, 10, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("valueIsNan", math.NaN(), 0, 10, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("valueIsNan", math.NaN(), 0, 10, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("valueIsNanInfRange", math.NaN(), math.Inf(-1), math.Inf(1), pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("valueIsNanInfRange", math.NaN(), math.Inf(-1), math.Inf(1), pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("valueIsNanInfRange", math.NaN(), math.Inf(-1), math.Inf(1), pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("valueIsNanInfRange", math.NaN(), math.Inf(-1), math.Inf(1), pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("infinityMax", math.Inf(1), 0, math.Inf(1), pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("infinityMax", math.Inf(1), 0, math.Inf(1), pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("infinityMin", math.Inf(-1), math.Inf(-1), 0, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("infinityMin", math.Inf(-1), math.Inf(-1), 0, pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("allAreNan", math.NaN(), math.NaN(), math.NaN(), pb.DoubleRangeFilter_NONE),
+		simpleDoubleRange("allAreNan", math.NaN(), math.NaN(), math.NaN(), pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("allAreNan", math.NaN(), math.NaN(), math.NaN(), pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("allAreNan", math.NaN(), math.NaN(), math.NaN(), pb.DoubleRangeFilter_BOTH),
+
+		simpleDoubleRange("valueIsMax", 1, 0, 1, pb.DoubleRangeFilter_MAX),
+		simpleDoubleRange("valueIsMin", 0, 0, 1, pb.DoubleRangeFilter_MIN),
+		simpleDoubleRange("excludeBoth", 0, 0, 1, pb.DoubleRangeFilter_BOTH),
+		simpleDoubleRange("excludeBoth", 1, 0, 1, pb.DoubleRangeFilter_BOTH),
 
 		{
 			"String equals simple negative", // and case sensitivity
@@ -329,7 +392,7 @@ func ExcludedTestCases() []TestCase {
 	}
 }
 
-func simpleDoubleRange(name string, value, min, max float64) TestCase {
+func simpleDoubleRange(name string, value, min, max float64, exclude pb.DoubleRangeFilter_Exclude) TestCase {
 	return TestCase{
 		"double range " + name,
 		&pb.Ticket{
@@ -345,6 +408,7 @@ func simpleDoubleRange(name string, value, min, max float64) TestCase {
 					DoubleArg: "field",
 					Min:       min,
 					Max:       max,
+					Exclude:   exclude,
 				},
 			},
 		},