Repository: mlange-42/arche
Pull request #271 (2023-05-04 21:42:30)

Rework generic query access
* Generic filters can use `filter.Query(&world, target)` optionally
* Target argument in generic filter `WithRelation` is optional
* Remove query and generic query `RelationUnchecked` (runtime overhead vanished for some reason)
* Remove generic filter method `Filter`

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

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

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/59644748/badge)](https://coveralls.io/builds/59644748) |
| :-- | --: |
| Change from base [Build 4887512548](https://coveralls.io/builds/59644262): |  0.0% |
| Covered Lines: | 3782 |
| Relevant Lines: | 3782 |

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 015ee1d..b54f838 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,13 +4,14 @@
 
 ### Highlights
 
-* Support for entity relations that can be queries like components (#231)
+* Support for entity relations that can be queries like components (#231, #271)
 
 ### Breaking changes
 
 * Removed `World.Batch` for entity batch creation, use `Builder` instead (#239)
 * Rework of generic entity creation API, use `MapX.New`, `MapX.NewWith`, `MapX.NewBatch` and `MapX.NewQuery` (#239, #252)
 * Stats object `WorldStats` etc. adapted for new structure of archetypes nested in nodes (#258)
+* Removed generic filter method `FilterX.Filter` (#271)
 
 ### Features
 
@@ -34,6 +35,7 @@
 * Optimize `RelationFilter`: get archetype directly instead of iterating complete node (#251)
 * Cached filters use swap-remove when removing an archetype (#253)
 * Speed up generic query re-compilation after changing the relation target (#255)
+* Speed up archetype and node iteration to be as fast as before the new nested structure (#270)
 
 ### Documentation
 
diff --git a/ecs/query.go b/ecs/query.go
index fc993af..f1bb599 100644
--- a/ecs/query.go
+++ b/ecs/query.go
@@ -124,7 +124,7 @@ func (q *Query) Relation(comp ID) Entity {
 //
 // GetRelationUnchecked is an optimized version of [Query.Relation].
 // Does not check that the component ID is applicable.
-func (q *Query) RelationUnchecked(comp ID) Entity {
+func (q *Query) relationUnchecked(comp ID) Entity {
 	return q.access.RelationTarget
 }
 
diff --git a/generic/compiled.go b/generic/compiled.go
index f7f674c..fcf3442 100644
--- a/generic/compiled.go
+++ b/generic/compiled.go
@@ -15,7 +15,9 @@ type compiledQuery struct {
 	cachedFilter   ecs.CachedFilter
 	filter         ecs.Filter
 	Ids            []ecs.ID
-	TargetID       int8
+	TargetComp     int8
+	Target         ecs.Entity
+	HasTarget      bool
 	compiled       bool
 	targetCompiled bool
 	locked         bool
@@ -23,58 +25,58 @@ type compiledQuery struct {
 
 func newCompiledQuery() compiledQuery {
 	return compiledQuery{
-		TargetID: -1,
+		TargetComp: -1,
 	}
 }
 
 // Compile compiles a generic filter.
-func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp, targetType Comp, target ecs.Entity) {
-	if q.targetCompiled {
+func (q *compiledQuery) Compile(w *ecs.World, include, optional, exclude []Comp, targetType Comp, target ecs.Entity, hasTarget bool) {
+	if q.compiled {
 		return
 	}
 
-	if !q.compiled {
-		q.Ids = toIds(w, include)
-		q.maskFilter = ecs.MaskFilter{
-			Include: toMaskOptional(w, q.Ids, optional),
-			Exclude: toMask(w, exclude),
-		}
+	q.Ids = toIds(w, include)
+	q.maskFilter = ecs.MaskFilter{
+		Include: toMaskOptional(w, q.Ids, optional),
+		Exclude: toMask(w, exclude),
 	}
 
 	if targetType == nil {
 		q.filter = &q.maskFilter
-		q.TargetID = -1
+		q.TargetComp = -1
 	} else {
+
 		targetID := ecs.TypeID(w, targetType)
 
-		if targetID != uint8(q.TargetID) {
-			q.TargetID = int8(targetID)
+		q.TargetComp = int8(targetID)
 
-			if !q.maskFilter.Include.Get(targetID) {
-				panic(fmt.Sprintf("relation component %v not in filter", targetType))
-			}
-			isRelation := false
-			if targetType.NumField() > 0 {
-				field := targetType.Field(0)
-				isRelation = field.Type == relationType && field.Name == relationType.Name()
-			}
-			if !isRelation {
-				panic(fmt.Sprintf("component type %v is not a relation", targetType))
-			}
+		if !q.maskFilter.Include.Get(targetID) {
+			panic(fmt.Sprintf("relation component %v not in filter", targetType))
+		}
+		isRelation := false
+		if targetType.NumField() > 0 {
+			field := targetType.Field(0)
+			isRelation = field.Type == relationType && field.Name == relationType.Name()
+		}
+		if !isRelation {
+			panic(fmt.Sprintf("component type %v is not a relation", targetType))
 		}
 
-		q.filter = ecs.RelationFilter(&q.maskFilter, target)
+		if hasTarget {
+			q.HasTarget = true
+			q.Target = target
+			q.filter = ecs.RelationFilter(&q.maskFilter, target)
+		} else {
+			q.filter = &q.maskFilter
+		}
 	}
 	q.targetCompiled = true
 	q.compiled = true
 }
 
 // Reset sets the compiledQuery to not compiled.
-func (q *compiledQuery) Reset(targetOnly bool) {
-	q.targetCompiled = false
-	if !targetOnly {
-		q.compiled = false
-	}
+func (q *compiledQuery) Reset() {
+	q.compiled = false
 }
 
 // Register the compiledQuery for caching.
diff --git a/generic/generate/query.go.txt b/generic/generate/query.go.txt
index 857eb7d..96dd2c3 100644
--- a/generic/generate/query.go.txt
+++ b/generic/generate/query.go.txt
@@ -38,7 +38,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) Optional(mask ...Comp) *Filter{{ .Index
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 {{ end }}
@@ -51,7 +51,7 @@ func (q *Filter{{ .Index }}{{ .Types }}) With(mask ...Comp) *Filter{{ .Index }}{
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -63,47 +63,64 @@ func (q *Filter{{ .Index }}{{ .Types }}) Without(mask ...Comp) *Filter{{ .Index
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter{{ .Index }}.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ecs.Entity) *Filter{{ .Index }}{{ .Types }} {
+func (q *Filter{{ .Index }}{{ .Types }}) WithRelation(comp Comp, target ...ecs.Entity) *Filter{{ .Index }}{{ .Types }} {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query{{ .Index }}] query for iteration.
-func (q *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World) Query{{ .Index }}{{ .Types }} {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query{{ .Index }}] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter{{ .Index }}.WithRelation] was not called
+//   - if the target was already set via [Filter{{ .Index }}.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter{{ .Index }}{{ .Types }}) Query(w *ecs.World, target ...ecs.Entity) Query{{ .Index }}{{ .Types }} {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+	
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query{{ .Index }}{{ .Types }}{
-		Query: w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query: w.Query(filter),
+		target: q.compiled.TargetComp,
 		{{ .IDAssign }}
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter{{ .Index }}.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter{{ .Index }}.Query].
-func (q *Filter{{ .Index }}{{ .Types }}) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter{{ .Index }}{{ .Types }}) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -147,7 +164,7 @@ func (q *Query{{ .Index }}{{ .Types }}) Get() ({{ .TypesReturn }}) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter{{ .Index }}] was not prepared for relations
 // using [Filter{{ .Index }}.WithRelation].
 func (q *Query{{ .Index }}{{ .Types }}) Relation() ecs.Entity {
@@ -156,14 +173,3 @@ func (q *Query{{ .Index }}{{ .Types }}) Relation() ecs.Entity {
 	}
 	return q.Query.Relation(ecs.ID(q.target))
 }
-
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query{{ .Index }}.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query{{ .Index }}{{ .Types }}) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
diff --git a/generic/query_generated.go b/generic/query_generated.go
index f9a79c4..d93468d 100644
--- a/generic/query_generated.go
+++ b/generic/query_generated.go
@@ -39,7 +39,7 @@ func (q *Filter0) With(mask ...Comp) *Filter0 {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -51,46 +51,63 @@ func (q *Filter0) Without(mask ...Comp) *Filter0 {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter0.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter0) WithRelation(comp Comp, target ecs.Entity) *Filter0 {
+func (q *Filter0) WithRelation(comp Comp, target ...ecs.Entity) *Filter0 {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query0] query for iteration.
-func (q *Filter0) Query(w *ecs.World) Query0 {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return Query0{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+// Query builds a [Query0] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter0.WithRelation] was not called
+//   - if the target was already set via [Filter0.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter0) Query(w *ecs.World, target ...ecs.Entity) Query0 {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
 	}
-}
 
-// Filter generates and return the [ecs.Filter] used after [Filter0.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter0.Query].
-func (q *Filter0) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
+	return Query0{
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
+	}
 }
 
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter0) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -124,7 +141,7 @@ type Query0 struct {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter0] was not prepared for relations
 // using [Filter0.WithRelation].
 func (q *Query0) Relation() ecs.Entity {
@@ -134,17 +151,6 @@ func (q *Query0) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query0.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query0) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter1 is a helper for building [Query1] query iterators.
@@ -183,7 +189,7 @@ func (q *Filter1[A]) Optional(mask ...Comp) *Filter1[A] {
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -195,7 +201,7 @@ func (q *Filter1[A]) With(mask ...Comp) *Filter1[A] {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -207,47 +213,64 @@ func (q *Filter1[A]) Without(mask ...Comp) *Filter1[A] {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter1.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter1[A]) WithRelation(comp Comp, target ecs.Entity) *Filter1[A] {
+func (q *Filter1[A]) WithRelation(comp Comp, target ...ecs.Entity) *Filter1[A] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query1] query for iteration.
-func (q *Filter1[A]) Query(w *ecs.World) Query1[A] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query1] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter1.WithRelation] was not called
+//   - if the target was already set via [Filter1.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter1[A]) Query(w *ecs.World, target ...ecs.Entity) Query1[A] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query1[A]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter1.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter1.Query].
-func (q *Filter1[A]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter1[A]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -289,7 +312,7 @@ func (q *Query1[A]) Get() *A {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter1] was not prepared for relations
 // using [Filter1.WithRelation].
 func (q *Query1[A]) Relation() ecs.Entity {
@@ -299,17 +322,6 @@ func (q *Query1[A]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query1.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query1[A]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter2 is a helper for building [Query2] query iterators.
@@ -349,7 +361,7 @@ func (q *Filter2[A, B]) Optional(mask ...Comp) *Filter2[A, B] {
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -361,7 +373,7 @@ func (q *Filter2[A, B]) With(mask ...Comp) *Filter2[A, B] {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -373,48 +385,65 @@ func (q *Filter2[A, B]) Without(mask ...Comp) *Filter2[A, B] {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter2.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter2[A, B]) WithRelation(comp Comp, target ecs.Entity) *Filter2[A, B] {
+func (q *Filter2[A, B]) WithRelation(comp Comp, target ...ecs.Entity) *Filter2[A, B] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query2] query for iteration.
-func (q *Filter2[A, B]) Query(w *ecs.World) Query2[A, B] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query2] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter2.WithRelation] was not called
+//   - if the target was already set via [Filter2.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter2[A, B]) Query(w *ecs.World, target ...ecs.Entity) Query2[A, B] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query2[A, B]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter2.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter2.Query].
-func (q *Filter2[A, B]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter2[A, B]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -458,7 +487,7 @@ func (q *Query2[A, B]) Get() (*A, *B) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter2] was not prepared for relations
 // using [Filter2.WithRelation].
 func (q *Query2[A, B]) Relation() ecs.Entity {
@@ -468,17 +497,6 @@ func (q *Query2[A, B]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query2.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query2[A, B]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter3 is a helper for building [Query3] query iterators.
@@ -519,7 +537,7 @@ func (q *Filter3[A, B, C]) Optional(mask ...Comp) *Filter3[A, B, C] {
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -531,7 +549,7 @@ func (q *Filter3[A, B, C]) With(mask ...Comp) *Filter3[A, B, C] {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -543,49 +561,66 @@ func (q *Filter3[A, B, C]) Without(mask ...Comp) *Filter3[A, B, C] {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter3.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter3[A, B, C]) WithRelation(comp Comp, target ecs.Entity) *Filter3[A, B, C] {
+func (q *Filter3[A, B, C]) WithRelation(comp Comp, target ...ecs.Entity) *Filter3[A, B, C] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query3] query for iteration.
-func (q *Filter3[A, B, C]) Query(w *ecs.World) Query3[A, B, C] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query3] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter3.WithRelation] was not called
+//   - if the target was already set via [Filter3.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter3[A, B, C]) Query(w *ecs.World, target ...ecs.Entity) Query3[A, B, C] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query3[A, B, C]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter3.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter3.Query].
-func (q *Filter3[A, B, C]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter3[A, B, C]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -631,7 +666,7 @@ func (q *Query3[A, B, C]) Get() (*A, *B, *C) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter3] was not prepared for relations
 // using [Filter3.WithRelation].
 func (q *Query3[A, B, C]) Relation() ecs.Entity {
@@ -641,17 +676,6 @@ func (q *Query3[A, B, C]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query3.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query3[A, B, C]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter4 is a helper for building [Query4] query iterators.
@@ -693,7 +717,7 @@ func (q *Filter4[A, B, C, D]) Optional(mask ...Comp) *Filter4[A, B, C, D] {
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -705,7 +729,7 @@ func (q *Filter4[A, B, C, D]) With(mask ...Comp) *Filter4[A, B, C, D] {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -717,29 +741,55 @@ func (q *Filter4[A, B, C, D]) Without(mask ...Comp) *Filter4[A, B, C, D] {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter4.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter4[A, B, C, D]) WithRelation(comp Comp, target ecs.Entity) *Filter4[A, B, C, D] {
+func (q *Filter4[A, B, C, D]) WithRelation(comp Comp, target ...ecs.Entity) *Filter4[A, B, C, D] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query4] query for iteration.
-func (q *Filter4[A, B, C, D]) Query(w *ecs.World) Query4[A, B, C, D] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query4] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter4.WithRelation] was not called
+//   - if the target was already set via [Filter4.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter4[A, B, C, D]) Query(w *ecs.World, target ...ecs.Entity) Query4[A, B, C, D] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query4[A, B, C, D]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
@@ -747,20 +797,11 @@ func (q *Filter4[A, B, C, D]) Query(w *ecs.World) Query4[A, B, C, D] {
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter4.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter4.Query].
-func (q *Filter4[A, B, C, D]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter4[A, B, C, D]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -808,7 +849,7 @@ func (q *Query4[A, B, C, D]) Get() (*A, *B, *C, *D) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter4] was not prepared for relations
 // using [Filter4.WithRelation].
 func (q *Query4[A, B, C, D]) Relation() ecs.Entity {
@@ -818,17 +859,6 @@ func (q *Query4[A, B, C, D]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query4.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query4[A, B, C, D]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter5 is a helper for building [Query5] query iterators.
@@ -871,7 +901,7 @@ func (q *Filter5[A, B, C, D, E]) Optional(mask ...Comp) *Filter5[A, B, C, D, E]
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -883,7 +913,7 @@ func (q *Filter5[A, B, C, D, E]) With(mask ...Comp) *Filter5[A, B, C, D, E] {
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -895,29 +925,55 @@ func (q *Filter5[A, B, C, D, E]) Without(mask ...Comp) *Filter5[A, B, C, D, E] {
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter5.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ecs.Entity) *Filter5[A, B, C, D, E] {
+func (q *Filter5[A, B, C, D, E]) WithRelation(comp Comp, target ...ecs.Entity) *Filter5[A, B, C, D, E] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query5] query for iteration.
-func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World) Query5[A, B, C, D, E] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query5] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter5.WithRelation] was not called
+//   - if the target was already set via [Filter5.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World, target ...ecs.Entity) Query5[A, B, C, D, E] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query5[A, B, C, D, E]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
@@ -926,20 +982,11 @@ func (q *Filter5[A, B, C, D, E]) Query(w *ecs.World) Query5[A, B, C, D, E] {
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter5.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter5.Query].
-func (q *Filter5[A, B, C, D, E]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter5[A, B, C, D, E]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -989,7 +1036,7 @@ func (q *Query5[A, B, C, D, E]) Get() (*A, *B, *C, *D, *E) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter5] was not prepared for relations
 // using [Filter5.WithRelation].
 func (q *Query5[A, B, C, D, E]) Relation() ecs.Entity {
@@ -999,17 +1046,6 @@ func (q *Query5[A, B, C, D, E]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query5.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query5[A, B, C, D, E]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter6 is a helper for building [Query6] query iterators.
@@ -1053,7 +1089,7 @@ func (q *Filter6[A, B, C, D, E, F]) Optional(mask ...Comp) *Filter6[A, B, C, D,
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1065,7 +1101,7 @@ func (q *Filter6[A, B, C, D, E, F]) With(mask ...Comp) *Filter6[A, B, C, D, E, F
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1077,29 +1113,55 @@ func (q *Filter6[A, B, C, D, E, F]) Without(mask ...Comp) *Filter6[A, B, C, D, E
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter6.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ecs.Entity) *Filter6[A, B, C, D, E, F] {
+func (q *Filter6[A, B, C, D, E, F]) WithRelation(comp Comp, target ...ecs.Entity) *Filter6[A, B, C, D, E, F] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query6] query for iteration.
-func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World) Query6[A, B, C, D, E, F] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query6] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter6.WithRelation] was not called
+//   - if the target was already set via [Filter6.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World, target ...ecs.Entity) Query6[A, B, C, D, E, F] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query6[A, B, C, D, E, F]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
@@ -1109,20 +1171,11 @@ func (q *Filter6[A, B, C, D, E, F]) Query(w *ecs.World) Query6[A, B, C, D, E, F]
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter6.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter6.Query].
-func (q *Filter6[A, B, C, D, E, F]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter6[A, B, C, D, E, F]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -1174,7 +1227,7 @@ func (q *Query6[A, B, C, D, E, F]) Get() (*A, *B, *C, *D, *E, *F) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter6] was not prepared for relations
 // using [Filter6.WithRelation].
 func (q *Query6[A, B, C, D, E, F]) Relation() ecs.Entity {
@@ -1184,17 +1237,6 @@ func (q *Query6[A, B, C, D, E, F]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query6.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query6[A, B, C, D, E, F]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter7 is a helper for building [Query7] query iterators.
@@ -1239,7 +1281,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) Optional(mask ...Comp) *Filter7[A, B, C,
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1251,7 +1293,7 @@ func (q *Filter7[A, B, C, D, E, F, G]) With(mask ...Comp) *Filter7[A, B, C, D, E
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1263,29 +1305,55 @@ func (q *Filter7[A, B, C, D, E, F, G]) Without(mask ...Comp) *Filter7[A, B, C, D
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter7.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ecs.Entity) *Filter7[A, B, C, D, E, F, G] {
+func (q *Filter7[A, B, C, D, E, F, G]) WithRelation(comp Comp, target ...ecs.Entity) *Filter7[A, B, C, D, E, F, G] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query7] query for iteration.
-func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World) Query7[A, B, C, D, E, F, G] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query7] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter7.WithRelation] was not called
+//   - if the target was already set via [Filter7.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World, target ...ecs.Entity) Query7[A, B, C, D, E, F, G] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query7[A, B, C, D, E, F, G]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
@@ -1296,20 +1364,11 @@ func (q *Filter7[A, B, C, D, E, F, G]) Query(w *ecs.World) Query7[A, B, C, D, E,
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter7.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter7.Query].
-func (q *Filter7[A, B, C, D, E, F, G]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter7[A, B, C, D, E, F, G]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -1363,7 +1422,7 @@ func (q *Query7[A, B, C, D, E, F, G]) Get() (*A, *B, *C, *D, *E, *F, *G) {
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter7] was not prepared for relations
 // using [Filter7.WithRelation].
 func (q *Query7[A, B, C, D, E, F, G]) Relation() ecs.Entity {
@@ -1373,17 +1432,6 @@ func (q *Query7[A, B, C, D, E, F, G]) Relation() ecs.Entity {
 	return q.Query.Relation(ecs.ID(q.target))
 }
 
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query7.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query7[A, B, C, D, E, F, G]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
-
 //////////////////////////////////////////////////////////////////////////
 
 // Filter8 is a helper for building [Query8] query iterators.
@@ -1429,7 +1477,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Optional(mask ...Comp) *Filter8[A, B,
 		panic("can't modify a registered filter")
 	}
 	q.optional = append(q.optional, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1441,7 +1489,7 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) With(mask ...Comp) *Filter8[A, B, C, D
 		panic("can't modify a registered filter")
 	}
 	q.include = append(q.include, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
@@ -1453,29 +1501,55 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Without(mask ...Comp) *Filter8[A, B, C
 		panic("can't modify a registered filter")
 	}
 	q.exclude = append(q.exclude, mask...)
-	q.compiled.Reset(false)
+	q.compiled.Reset()
 	return q
 }
 
-// WithRelation restricts the query to entities that have the given relation.
+// WithRelation sets the filter's [ecs.Relation] component and optionally
+// restricts the query to entities that have the given relation target.
+//
+// Use without the optional argument to specify the relation target in [Filter8.Query].
+// If the optional argument is provided, the filter's relation target is set permanently.
 //
 // Create the required component ID with [T].
-func (q *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ecs.Entity) *Filter8[A, B, C, D, E, F, G, H] {
+func (q *Filter8[A, B, C, D, E, F, G, H]) WithRelation(comp Comp, target ...ecs.Entity) *Filter8[A, B, C, D, E, F, G, H] {
 	if q.compiled.locked {
 		panic("can't modify a registered filter")
 	}
 	q.targetType = comp
-	q.target = target
-	q.compiled.Reset(true)
+	if len(target) > 0 {
+		q.target = target[0]
+		q.hasTarget = true
+	}
+	q.compiled.Reset()
 	return q
 }
 
-// Query builds a [Query8] query for iteration.
-func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World) Query8[A, B, C, D, E, F, G, H] {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+// Query builds a [Query8] query for iteration, with an optional relation target.
+//
+// A relation target can't be used:
+//   - if [Filter8.WithRelation] was not called
+//   - if the target was already set via [Filter8.WithRelation]
+//   - if the filter is registered for caching
+//
+// Panics in these cases.
+func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World, target ...ecs.Entity) Query8[A, B, C, D, E, F, G, H] {
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
+
+	filter := q.compiled.filter
+	if len(target) > 0 {
+		if q.compiled.locked {
+			panic("can't change relation target on a cached query")
+		}
+		if q.hasTarget {
+			panic("can't change relation target on a query with fixed target")
+		}
+		filter = ecs.RelationFilter(&q.compiled.maskFilter, target[0])
+	}
+
 	return Query8[A, B, C, D, E, F, G, H]{
-		Query:  w.Query(q.compiled.filter),
-		target: q.compiled.TargetID,
+		Query:  w.Query(filter),
+		target: q.compiled.TargetComp,
 		id0:    q.compiled.Ids[0],
 		id1:    q.compiled.Ids[1],
 		id2:    q.compiled.Ids[2],
@@ -1487,20 +1561,11 @@ func (q *Filter8[A, B, C, D, E, F, G, H]) Query(w *ecs.World) Query8[A, B, C, D,
 	}
 }
 
-// Filter generates and return the [ecs.Filter] used after [Filter8.Query].
-//
-// Can be passed to [ecs.World.Query].
-// For the intended generic use, however, generate a generic query with [Filter8.Query].
-func (q *Filter8[A, B, C, D, E, F, G, H]) Filter(w *ecs.World) ecs.MaskFilter {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
-	return q.compiled.maskFilter
-}
-
 // Register the filter for caching.
 //
 // See [ecs.Cache] for details on filter caching.
 func (q *Filter8[A, B, C, D, E, F, G, H]) Register(w *ecs.World) {
-	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target)
+	q.compiled.Compile(w, q.include, q.optional, q.exclude, q.targetType, q.target, q.hasTarget)
 	q.compiled.Register(w)
 }
 
@@ -1556,7 +1621,7 @@ func (q *Query8[A, B, C, D, E, F, G, H]) Get() (*A, *B, *C, *D, *E, *F, *G, *H)
 
 // Relation returns the target entity for the query's relation.
 //
-// Panics if the entity does not have the given component, or if the component is not a [Relation].
+// Panics if the entity does not have the given component, or if the component is not an [ecs.Relation].
 // Panics if the underlying [Filter8] was not prepared for relations
 // using [Filter8.WithRelation].
 func (q *Query8[A, B, C, D, E, F, G, H]) Relation() ecs.Entity {
@@ -1565,14 +1630,3 @@ func (q *Query8[A, B, C, D, E, F, G, H]) Relation() ecs.Entity {
 	}
 	return q.Query.Relation(ecs.ID(q.target))
 }
-
-// RelationUnchecked returns the target entity for the query's relation.
-//
-// Returns the zero entity if the entity does not have the given component,
-// or if the component is not an [ecs.Relation].
-//
-// RelationUnchecked is an optimized version of [Query8.Relation].
-// Does not check that the component ID is applicable.
-func (q *Query8[A, B, C, D, E, F, G, H]) RelationUnchecked() ecs.Entity {
-	return q.Query.RelationUnchecked(ecs.ID(q.target))
-}
diff --git a/generic/util.go b/generic/util.go
index 8f76507..459025b 100644
--- a/generic/util.go
+++ b/generic/util.go
@@ -13,6 +13,7 @@ type filter struct {
 	exclude    []Comp
 	targetType Comp
 	target     ecs.Entity
+	hasTarget  bool
 	compiled   compiledQuery
 }
 


Test patch:
diff --git a/benchmark/arche/relations/relation_test.go b/benchmark/arche/relations/relation_test.go
index 1124629..87f3341 100644
--- a/benchmark/arche/relations/relation_test.go
+++ b/benchmark/arche/relations/relation_test.go
@@ -29,13 +29,11 @@ func benchmarkRelation(b *testing.B, numParents int, numChildren int) {
 		}
 	}
 
-	comp := generic.T[ChildRelation]()
 	parentFilter := generic.NewFilter1[ParentList]()
 	parentFilter.Register(&world)
 
-	childFilter := generic.NewFilter1[ChildRelation]().WithRelation(comp, ecs.Entity{})
-	childF := childFilter.Filter(&world)
 	childID := ecs.ComponentID[ChildRelation](&world)
+	childF := ecs.All(childID)
 
 	b.StartTimer()
 
diff --git a/ecs/query_test.go b/ecs/query_test.go
index a29045e..68c0718 100644
--- a/ecs/query_test.go
+++ b/ecs/query_test.go
@@ -351,7 +351,7 @@ func TestQueryRelations(t *testing.T) {
 		targ2 := query.Relation(relID)
 
 		assert.Equal(t, targ, targ2)
-		assert.Equal(t, targ, query.RelationUnchecked(relID))
+		assert.Equal(t, targ, query.relationUnchecked(relID))
 
 		assert.Panics(t, func() { query.Relation(rel2ID) })
 		assert.Panics(t, func() { query.Relation(posID) })
diff --git a/ecs/relation_benchmark_test.go b/ecs/relation_benchmark_test.go
index a6241fd..e1b9e56 100644
--- a/ecs/relation_benchmark_test.go
+++ b/ecs/relation_benchmark_test.go
@@ -47,7 +47,7 @@ func benchmarkRelationGetQueryUnchecked(b *testing.B, count int) {
 	for i := 0; i < b.N; i++ {
 		query := world.Query(filter)
 		for query.Next() {
-			tempTarget = query.RelationUnchecked(relID)
+			tempTarget = query.relationUnchecked(relID)
 		}
 	}
 
diff --git a/generic/query_test.go b/generic/query_test.go
index eab5906..a6c2685 100644
--- a/generic/query_test.go
+++ b/generic/query_test.go
@@ -102,8 +102,6 @@ func TestQuery0(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -135,10 +133,23 @@ func TestQuery0(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter0().
+			With(T[testRelationA]()).
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	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.WithRelation(T[testRelationA](), targ) })
@@ -171,8 +182,6 @@ func TestQuery1(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -204,10 +213,22 @@ func TestQuery1(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter1[testRelationA]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -243,8 +264,6 @@ func TestQuery2(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	for i := 0; i < 10; i++ {
@@ -280,10 +299,24 @@ func TestQuery2(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter2[
+			testStruct0, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -324,8 +357,6 @@ func TestQuery3(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -359,10 +390,24 @@ func TestQuery3(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter3[
+			testStruct0, testStruct1, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -407,8 +452,6 @@ func TestQuery4(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 3, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -445,10 +488,24 @@ func TestQuery4(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter4[
+			testStruct0, testStruct1, testStruct2, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -497,8 +554,6 @@ func TestQuery5(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 3, 4, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -537,10 +592,25 @@ func TestQuery5(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter5[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -593,8 +663,6 @@ func TestQuery6(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 3, 4, 5, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -634,10 +702,25 @@ func TestQuery6(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter6[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -697,8 +780,6 @@ func TestQuery7(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 3, 4, 5, 6, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -739,10 +820,25 @@ func TestQuery7(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter7[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 3, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
@@ -798,8 +894,6 @@ func TestQuery8(t *testing.T) {
 			With(T[testStruct8]()).
 			Without(T[testStruct9]())
 
-	f := ecs.All(0, 2, 3, 4, 5, 6, 7, 8).Without(9)
-	assert.Equal(t, f, filter.Filter(&w))
 	filter.Register(&w)
 
 	query := filter.Query(&w)
@@ -841,10 +935,25 @@ func TestQuery8(t *testing.T) {
 	for q.Next() {
 		trg := q.Relation()
 		assert.Equal(t, targ, trg)
-		assert.Equal(t, targ, q.RelationUnchecked())
 	}
 
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
+
+	filter2 =
+		NewFilter8[
+			testStruct0, testStruct1, testStruct2, testStruct3,
+			testStruct4, testStruct5, testStruct6, testRelationA,
+		]().
+			WithRelation(T[testRelationA]())
+	q = filter2.Query(&w)
+	assert.Equal(t, 2, q.Count())
+	q.Close()
+	q = filter2.Query(&w, targ)
+	assert.Equal(t, 1, q.Count())
+	q.Close()
+
 	filter2.Register(&w)
+	assert.Panics(t, func() { filter2.Query(&w, targ) })
 	assert.Panics(t, func() { filter2.Optional(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.With(T[testStruct0]()) })
 	assert.Panics(t, func() { filter2.Without(T[testStruct0]()) })
diff --git a/generic/relation_benchmark_test.go b/generic/relation_benchmark_test.go
new file mode 100644
index 0000000..2adab79
--- /dev/null
+++ b/generic/relation_benchmark_test.go
@@ -0,0 +1,170 @@
+package generic_test
+
+import (
+	"testing"
+
+	"github.com/mlange-42/arche/ecs"
+	"github.com/mlange-42/arche/generic"
+)
+
+type testRelationA struct {
+	ecs.Relation
+}
+
+func benchmarkRelationGetQuery(b *testing.B, count int) {
+	b.StopTimer()
+
+	world := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024).WithRelationCapacityIncrement(128))
+	relID := ecs.ComponentID[testRelationA](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, relID).WithRelation(relID)
+	builder.NewBatch(count, target)
+
+	filter := generic.NewFilter1[testRelationA]().WithRelation(generic.T[testRelationA](), target)
+	b.StartTimer()
+
+	var tempTarget ecs.Entity
+	for i := 0; i < b.N; i++ {
+		query := filter.Query(&world)
+		for query.Next() {
+			tempTarget = query.Relation()
+		}
+	}
+
+	_ = tempTarget
+}
+
+func benchmarkRelationGetWorld(b *testing.B, count int) {
+	b.StopTimer()
+
+	world := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024).WithRelationCapacityIncrement(128))
+	relID := ecs.ComponentID[testRelationA](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, relID).WithRelation(relID)
+	q := builder.NewQuery(count, target)
+	entities := make([]ecs.Entity, 0, count)
+	for q.Next() {
+		entities = append(entities, q.Entity())
+	}
+
+	mapper := generic.NewMap[testRelationA](&world)
+	b.StartTimer()
+
+	var tempTarget ecs.Entity
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			tempTarget = mapper.GetRelation(e)
+		}
+	}
+
+	_ = tempTarget
+}
+
+func benchmarkRelationGetWorldUnchecked(b *testing.B, count int) {
+	b.StopTimer()
+
+	world := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024).WithRelationCapacityIncrement(128))
+	relID := ecs.ComponentID[testRelationA](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, relID).WithRelation(relID)
+	q := builder.NewQuery(count, target)
+	entities := make([]ecs.Entity, 0, count)
+	for q.Next() {
+		entities = append(entities, q.Entity())
+	}
+	mapper := generic.NewMap[testRelationA](&world)
+	b.StartTimer()
+
+	var tempTarget ecs.Entity
+	for i := 0; i < b.N; i++ {
+		for _, e := range entities {
+			tempTarget = mapper.GetRelationUnchecked(e)
+		}
+	}
+
+	_ = tempTarget
+}
+
+func benchmarkRelationSet(b *testing.B, count int) {
+	b.StopTimer()
+
+	world := ecs.NewWorld(ecs.NewConfig().WithCapacityIncrement(1024).WithRelationCapacityIncrement(128))
+	relID := ecs.ComponentID[testRelationA](&world)
+
+	target := world.NewEntity()
+
+	builder := ecs.NewBuilder(&world, relID).WithRelation(relID)
+	q := builder.NewQuery(count)
+	entities := make([]ecs.Entity, 0, count)
+	for q.Next() {
+		entities = append(entities, q.Entity())
+	}
+	b.StartTimer()
+
+	var tempTarget ecs.Entity
+	for i := 0; i < b.N; i++ {
+		trg := ecs.Entity{}
+		if i%2 == 0 {
+			trg = target
+		}
+		for _, e := range entities {
+			world.Relations().Set(e, relID, trg)
+		}
+	}
+
+	_ = tempTarget
+}
+
+func BenchmarkRelationGetQuery_1000(b *testing.B) {
+	benchmarkRelationGetQuery(b, 1000)
+}
+
+func BenchmarkRelationGetQuery_10000(b *testing.B) {
+	benchmarkRelationGetQuery(b, 10000)
+}
+
+func BenchmarkRelationGetQuery_100000(b *testing.B) {
+	benchmarkRelationGetQuery(b, 100000)
+}
+
+func BenchmarkRelationGetWorld_1000(b *testing.B) {
+	benchmarkRelationGetWorld(b, 1000)
+}
+
+func BenchmarkRelationGetWorld_10000(b *testing.B) {
+	benchmarkRelationGetWorld(b, 10000)
+}
+
+func BenchmarkRelationGetWorld_100000(b *testing.B) {
+	benchmarkRelationGetWorld(b, 100000)
+}
+
+func BenchmarkRelationGetWorldUnchecked_1000(b *testing.B) {
+	benchmarkRelationGetWorldUnchecked(b, 1000)
+}
+
+func BenchmarkRelationGetWorldUnchecked_10000(b *testing.B) {
+	benchmarkRelationGetWorldUnchecked(b, 10000)
+}
+
+func BenchmarkRelationGetWorldUnchecked_100000(b *testing.B) {
+	benchmarkRelationGetWorldUnchecked(b, 100000)
+}
+
+func BenchmarkRelationSet_1000(b *testing.B) {
+	benchmarkRelationSet(b, 1000)
+}
+
+func BenchmarkRelationSet_10000(b *testing.B) {
+	benchmarkRelationSet(b, 10000)
+}
+
+func BenchmarkRelationSet_100000(b *testing.B) {
+	benchmarkRelationSet(b, 100000)
+}