Repository: mlange-42/arche
Pull request #381 (2024-02-13 23:42:32)

Add method `FilterX.Exclusive()` to all generic filters

coveralls: ## Pull Request Test Coverage Report for [Build 7894219330](https://coveralls.io/builds/65681358)


### Details

* **0** of **165**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **100.0%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65681358/badge)](https://coveralls.io/builds/65681358) |
| :-- | --: |
| Change from base [Build 7865490814](https://coveralls.io/builds/65630026): |  0.0% |
| Covered Lines: | 6366 |
| Relevant Lines: | 6366 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdda8a4..6ccf8d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 * Adds method `Query.EntityAt()`, useful for things like random sampling of entities (#358)
 * Adds build tag `debug` to improve error messages in a few places where we rely on standard library panics for performance (#377)
+* Adds method `FilterX.Exclusive()` to all generic filters (#381)
 
 ### Documentation
 
@@ -18,6 +19,10 @@
 
 * Prevents using the same component multiple times in any operations, through panic (#357)
 
+### Performance
+
+* Generic filters use `Mask` instead of slower `MaskFilter` if no components are excluded (#381)
+
 ### Other
 
 * Improves error messages for running out of world locks, components or resources, and on unbalanced unlock (#363)
diff --git a/generic/_generate/query.go.txt b/generic/_generate/query.go.txt
index 348d310..677354a 100644
--- a/generic/_generate/query.go.txt
+++ b/generic/_generate/query.go.txt
@@ -62,11 +62,27 @@ func (f *Filter{{ .Index }}{{ .Types }}) Without(mask ...Comp) *Filter{{ .Index
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter{{ .Index }}{{ .Types }}) Exclusive() *Filter{{ .Index }}{{ .Types }} {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -96,7 +112,7 @@ func (f *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ...ecs.E
 //
 // Panics in these cases.
 func (f *Filter{{ .Index }}{{ .Types }}) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -136,7 +152,7 @@ func (f *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World, target ...ecs.Entit
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter{{ .Index }}{{ .Types }}) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
diff --git a/generic/compiled.go b/generic/compiled.go
index 72617cd..94bcce5 100644
--- a/generic/compiled.go
+++ b/generic/compiled.go
@@ -29,19 +29,32 @@ func newCompiledQuery() compiledQuery {
 }
 
 // Compile compiles a generic filter.
-func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp, targetType Comp, target ecs.Entity, hasTarget bool) {
+func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp, exclusive bool, targetType Comp, target ecs.Entity, hasTarget bool) {
 	if q.compiled {
 		return
 	}
 
 	q.Ids = toIds(w, include)
+
+	incl := toMaskOptional(w, q.Ids, optional)
+	var excl ecs.Mask
+	if exclusive {
+		excl = incl.Not()
+	} else {
+		excl = toMask(w, exclude)
+	}
 	q.maskFilter = ecs.MaskFilter{
-		Include: toMaskOptional(w, q.Ids, optional),
-		Exclude: toMask(w, exclude),
+		Include: incl,
+		Exclude: excl,
 	}
+	noExclude := !exclusive && len(exclude) == 0
 
 	if targetType == nil {
-		q.filter = &q.maskFilter
+		if noExclude {
+			q.filter = q.maskFilter.Include
+		} else {
+			q.filter = &q.maskFilter
+		}
 		q.Relation = ecs.ID{}
 		q.HasRelation = false
 	} else {
@@ -68,7 +81,11 @@ func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp,
 			q.relationFilter = ecs.NewRelationFilter(&q.maskFilter, target)
 			q.filter = &q.relationFilter
 		} else {
-			q.filter = &q.maskFilter
+			if noExclude {
+				q.filter = q.maskFilter.Include
+			} else {
+				q.filter = &q.maskFilter
+			}
 		}
 	}
 	q.targetCompiled = true
diff --git a/generic/query_generated.go b/generic/query_generated.go
index 8ee1989..afc1cd9 100644
--- a/generic/query_generated.go
+++ b/generic/query_generated.go
@@ -50,11 +50,27 @@ func (f *Filter0) Without(mask ...Comp) *Filter0 {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter0) Exclusive() *Filter0 {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -84,7 +100,7 @@ func (f *Filter0) WithRelation(comp Comp, target ...ecs.Entity) *Filter0 {
 //
 // Panics in these cases.
 func (f *Filter0) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -123,7 +139,7 @@ func (f *Filter0) Query(w *ecs.World, target ...ecs.Entity) Query0 {
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter0) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -229,11 +245,27 @@ func (f *Filter1[A]) Without(mask ...Comp) *Filter1[A] {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter1[A]) Exclusive() *Filter1[A] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -263,7 +295,7 @@ func (f *Filter1[A]) WithRelation(comp Comp, target ...ecs.Entity) *Filter1[A] {
 //
 // Panics in these cases.
 func (f *Filter1[A]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -303,7 +335,7 @@ func (f *Filter1[A]) Query(w *ecs.World, target ...ecs.Entity) Query1[A] {
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter1[A]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -418,11 +450,27 @@ func (f *Filter2[A, B]) Without(mask ...Comp) *Filter2[A, B] {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter2[A, B]) Exclusive() *Filter2[A, B] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -452,7 +500,7 @@ func (f *Filter2[A, B]) WithRelation(comp Comp, target ...ecs.Entity) *Filter2[A
 //
 // Panics in these cases.
 func (f *Filter2[A, B]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -493,7 +541,7 @@ func (f *Filter2[A, B]) Query(w *ecs.World, target ...ecs.Entity) Query2[A, B] {
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter2[A, B]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -611,11 +659,27 @@ func (f *Filter3[A, B, C]) Without(mask ...Comp) *Filter3[A, B, C] {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter3[A, B, C]) Exclusive() *Filter3[A, B, C] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -645,7 +709,7 @@ func (f *Filter3[A, B, C]) WithRelation(comp Comp, target ...ecs.Entity) *Filter
 //
 // Panics in these cases.
 func (f *Filter3[A, B, C]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -687,7 +751,7 @@ func (f *Filter3[A, B, C]) Query(w *ecs.World, target ...ecs.Entity) Query3[A, B
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter3[A, B, C]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -808,11 +872,27 @@ func (f *Filter4[A, B, C, D]) Without(mask ...Comp) *Filter4[A, B, C, D] {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter4[A, B, C, D]) Exclusive() *Filter4[A, B, C, D] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -842,7 +922,7 @@ func (f *Filter4[A, B, C, D]) WithRelation(comp Comp, target ...ecs.Entity) *Fil
 //
 // Panics in these cases.
 func (f *Filter4[A, B, C, D]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -885,7 +965,7 @@ func (f *Filter4[A, B, C, D]) Query(w *ecs.World, target ...ecs.Entity) Query4[A
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter4[A, B, C, D]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -1009,11 +1089,27 @@ func (f *Filter5[A, B, C, D, E]) Without(mask ...Comp) *Filter5[A, B, C, D, E] {
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter5[A, B, C, D, E]) Exclusive() *Filter5[A, B, C, D, E] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -1043,7 +1139,7 @@ func (f *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ...ecs.Entity) *
 //
 // Panics in these cases.
 func (f *Filter5[A, B, C, D, E]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -1087,7 +1183,7 @@ func (f *Filter5[A, B, C, D, E]) Query(w *ecs.World, target ...ecs.Entity) Query
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter5[A, B, C, D, E]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -1214,11 +1310,27 @@ func (f *Filter6[A, B, C, D, E, F]) Without(mask ...Comp) *Filter6[A, B, C, D, E
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter6[A, B, C, D, E, F]) Exclusive() *Filter6[A, B, C, D, E, F] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -1248,7 +1360,7 @@ func (f *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ...ecs.Entity
 //
 // Panics in these cases.
 func (f *Filter6[A, B, C, D, E, F]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -1293,7 +1405,7 @@ func (f *Filter6[A, B, C, D, E, F]) Query(w *ecs.World, target ...ecs.Entity) Qu
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter6[A, B, C, D, E, F]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -1423,11 +1535,27 @@ func (f *Filter7[A, B, C, D, E, F, G]) Without(mask ...Comp) *Filter7[A, B, C, D
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter7[A, B, C, D, E, F, G]) Exclusive() *Filter7[A, B, C, D, E, F, G] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -1457,7 +1585,7 @@ func (f *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ...ecs.Ent
 //
 // Panics in these cases.
 func (f *Filter7[A, B, C, D, E, F, G]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -1503,7 +1631,7 @@ func (f *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World, target ...ecs.Entity)
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter7[A, B, C, D, E, F, G]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -1636,11 +1764,27 @@ func (f *Filter8[A, B, C, D, E, F, G, H]) Without(mask ...Comp) *Filter8[A, B, C
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter8[A, B, C, D, E, F, G, H]) Exclusive() *Filter8[A, B, C, D, E, F, G, H] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -1670,7 +1814,7 @@ func (f *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ...ecs.
 //
 // Panics in these cases.
 func (f *Filter8[A, B, C, D, E, F, G, H]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -1717,7 +1861,7 @@ func (f *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World, target ...ecs.Enti
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter8[A, B, C, D, E, F, G, H]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -1853,11 +1997,27 @@ func (f *Filter9[A, B, C, D, E, F, G, H, I]) Without(mask ...Comp) *Filter9[A, B
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter9[A, B, C, D, E, F, G, H, I]) Exclusive() *Filter9[A, B, C, D, E, F, G, H, I] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -1887,7 +2047,7 @@ func (f *Filter9[A, B, C, D, E, F, G, H, I]) WithRelation(comp Comp, target ...e
 //
 // Panics in these cases.
 func (f *Filter9[A, B, C, D, E, F, G, H, I]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -1935,7 +2095,7 @@ func (f *Filter9[A, B, C, D, E, F, G, H, I]) Query(w *ecs.World, target ...ecs.E
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter9[A, B, C, D, E, F, G, H, I]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -2074,11 +2234,27 @@ func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) Without(mask ...Comp) *Filter10
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) Exclusive() *Filter10[A, B, C, D, E, F, G, H, I, J] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -2108,7 +2284,7 @@ func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) WithRelation(comp Comp, target
 //
 // Panics in these cases.
 func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -2157,7 +2333,7 @@ func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) Query(w *ecs.World, target ...e
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter10[A, B, C, D, E, F, G, H, I, J]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -2299,11 +2475,27 @@ func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Without(mask ...Comp) *Filte
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Exclusive() *Filter11[A, B, C, D, E, F, G, H, I, J, K] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -2333,7 +2525,7 @@ func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) WithRelation(comp Comp, targ
 //
 // Panics in these cases.
 func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -2383,7 +2575,7 @@ func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Query(w *ecs.World, target .
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter11[A, B, C, D, E, F, G, H, I, J, K]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
@@ -2528,11 +2720,27 @@ func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Without(mask ...Comp) *Fi
 	if f.compiled.locked {
 		panic("can't modify a registered filter")
 	}
+	if f.exclusive {
+		panic("filter is already exclusive")
+	}
 	f.exclude = append(f.exclude, mask...)
 	f.compiled.Reset()
 	return f
 }
 
+// Exclusive makes the filter exclusive in the sense that the component composition is matched exactly,
+// and no other components are allowed.
+func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Exclusive() *Filter12[A, B, C, D, E, F, G, H, I, J, K, L] {
+	if f.compiled.locked {
+		panic("can't modify a registered filter")
+	}
+	if len(f.exclude) > 0 {
+		panic("filter already excludes some components")
+	}
+	f.exclusive = true
+	return f
+}
+
 // WithRelation sets the filter's [ecs.Relation] component and optionally
 // restricts the query to entities that have the given relation target.
 //
@@ -2562,7 +2770,7 @@ func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) WithRelation(comp Comp, t
 //
 // Panics in these cases.
 func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Filter(w *ecs.World, target ...ecs.Entity) ecs.Filter {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 
 	filter := f.compiled.filter
 	if len(target) > 0 {
@@ -2613,7 +2821,7 @@ func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Query(w *ecs.World, targe
 //
 // See [ecs.Cache] for details on filter caching.
 func (f *Filter12[A, B, C, D, E, F, G, H, I, J, K, L]) Register(w *ecs.World) {
-	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.targetType, f.target, f.hasTarget)
+	f.compiled.Compile(w, f.include, f.optional, f.exclude, f.exclusive, f.targetType, f.target, f.hasTarget)
 	f.compiled.Register(w)
 }
 
diff --git a/generic/util.go b/generic/util.go
index 5872522..1566445 100644
--- a/generic/util.go
+++ b/generic/util.go
@@ -11,6 +11,7 @@ type filter struct {
 	include    []Comp
 	optional   []Comp
 	exclude    []Comp
+	exclusive  bool
 	targetType Comp
 	target     ecs.Entity
 	hasTarget  bool


Test patch:
diff --git a/generic/query_test.go b/generic/query_test.go
index 685b8e9..c10eb37 100644
--- a/generic/query_test.go
+++ b/generic/query_test.go
@@ -53,6 +53,14 @@ func TestQueryOptionalNot(t *testing.T) {
 	}
 	assert.Equal(t, 1, cnt)
 
+	query2 = NewFilter2[testStruct0, testStruct1]().Exclusive().Query(&w)
+	cnt = 0
+	for query2.Next() {
+		_, _ = query2.Get()
+		cnt++
+	}
+	assert.Equal(t, 1, cnt)
+
 	query2 = NewFilter2[testStruct0, testStruct1]().With(T[testStruct2]()).Without(T[testStruct9]()).Query(&w)
 	cnt = 0
 	for query2.Next() {
@@ -78,6 +86,21 @@ func TestQueryOptionalNot(t *testing.T) {
 	assert.Equal(t, 3, cnt)
 }
 
+func TestQueryRelation(t *testing.T) {
+	w := ecs.NewWorld()
+	mapper1 := NewMap2[testStruct0, testRelationA](&w, T[testRelationA]())
+	mapper2 := NewMap3[testStruct0, testStruct1, testRelationA](&w, T[testRelationA]())
+
+	parent := w.NewEntity()
+
+	mapper1.NewBatch(10, parent)
+	mapper2.NewBatch(10, parent)
+
+	query := NewFilter2[testStruct0, testRelationA]().Exclusive().WithRelation(T[testRelationA]()).Query(&w, parent)
+	assert.Equal(t, 10, query.Count())
+	query.Close()
+}
+
 func TestQuery0(t *testing.T) {
 	w := ecs.NewWorld()
 
@@ -102,6 +125,8 @@ func TestQuery0(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -152,8 +177,12 @@ func TestQuery0(t *testing.T) {
 	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 = NewFilter0().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery1(t *testing.T) {
@@ -182,6 +211,8 @@ func TestQuery1(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -232,8 +263,12 @@ func TestQuery1(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 = NewFilter1[testRelationA]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery2(t *testing.T) {
@@ -264,6 +299,8 @@ func TestQuery2(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	for i := 0; i < 10; i++ {
@@ -320,8 +357,15 @@ func TestQuery2(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter2[
+			testStruct0, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery3(t *testing.T) {
@@ -357,6 +401,8 @@ func TestQuery3(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -411,8 +457,15 @@ func TestQuery3(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter3[
+			testStruct0, testStruct1, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery4(t *testing.T) {
@@ -452,6 +505,8 @@ func TestQuery4(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -509,8 +564,15 @@ func TestQuery4(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter4[
+			testStruct0, testStruct1, testStruct2, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery5(t *testing.T) {
@@ -554,6 +616,8 @@ func TestQuery5(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -614,8 +678,16 @@ func TestQuery5(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter5[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery6(t *testing.T) {
@@ -663,6 +735,8 @@ func TestQuery6(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -724,8 +798,16 @@ func TestQuery6(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter6[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery7(t *testing.T) {
@@ -780,6 +862,8 @@ func TestQuery7(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -842,8 +926,16 @@ func TestQuery7(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter7[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery8(t *testing.T) {
@@ -894,6 +986,8 @@ func TestQuery8(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -957,8 +1051,16 @@ func TestQuery8(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter8[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery9(t *testing.T) {
@@ -1013,6 +1115,8 @@ func TestQuery9(t *testing.T) {
 			With(T[testStruct9]()).
 			Without(T[testStruct10]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -1079,8 +1183,17 @@ func TestQuery9(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter9[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testStruct7,
+			testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery10(t *testing.T) {
@@ -1138,6 +1251,8 @@ func TestQuery10(t *testing.T) {
 			With(T[testStruct10]()).
 			Without(T[testStruct11]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -1205,8 +1320,17 @@ func TestQuery10(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter10[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testStruct7,
+			testStruct8, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery11(t *testing.T) {
@@ -1267,6 +1391,8 @@ func TestQuery11(t *testing.T) {
 			With(T[testStruct11]()).
 			Without(T[testStruct12]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -1335,8 +1461,17 @@ func TestQuery11(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter11[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testStruct7,
+			testStruct8, testStruct9, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQuery12(t *testing.T) {
@@ -1400,6 +1535,8 @@ func TestQuery12(t *testing.T) {
 			With(T[testStruct12]()).
 			Without(T[testStruct13]())
 
+	assert.Panics(t, func() { filter.Exclusive() })
+
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -1469,8 +1606,17 @@ func TestQuery12(t *testing.T) {
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
+	assert.Panics(t, func() { filter2.Exclusive() })
 	assert.Panics(t, func() { filter2.WithRelation(T[testRelationA](), targ) })
 	filter2.Unregister(&w)
+
+	filter2 =
+		NewFilter12[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testStruct7,
+			testStruct8, testStruct9, testStruct10, testRelationA,
+		]().Exclusive()
+	assert.PanicsWithValue(t, "filter is already exclusive", func() { filter2.Without(T[testStruct13]()) })
 }
 
 func TestQueryGeneric(t *testing.T) {