Skip to content

Commit 14d333a

Browse files
committed
fix(query): support specific relation queries for dontFragment components
Fixed an issue where specific target relation queries didn't work properly with dontFragment: true relation components. For dontFragment relations, specific relation IDs are not stored in archetype.componentTypes, only the wildcard marker is added to the archetype. The query matching function matchesComponentTypes only checked archetype.componentTypes.includes(type), causing specific dontFragment relation queries to fail. Key changes: 1. Modified matchesComponentTypes function in filter.ts to recognize specific dontFragment relations, checking if the archetype contains the corresponding wildcard marker when querying specific dontFragment relations 2. Enhanced Query class with specificDontFragmentTypes cache to store specific dontFragment relations requiring entity-level filtering 3. Modified getEntities() method to enable slow path for queries containing specific dontFragment relations 4. Renamed entityHasAllWildcards() to entityMatchesQuery(), checking both wildcard relations and specific dontFragment relations All 261 tests pass, including the previously failing 2 tests: - "should support specific relation queries for dontFragment components" - "should support specific relation queries with exclusive dontFragment when target changes"
1 parent 7b69914 commit 14d333a

File tree

3 files changed

+116
-11
lines changed

3 files changed

+116
-11
lines changed

src/__tests__/dont-fragment-query-notification.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,4 +464,73 @@ describe("DontFragment Query Notification Issue", () => {
464464
expect(archetype.componentTypes).toContain(wildcardChildOf);
465465
}
466466
});
467+
468+
it("should support specific relation queries for dontFragment components", () => {
469+
const world = new World();
470+
471+
const PositionId = component();
472+
const ChildOf = component({ dontFragment: true });
473+
474+
const parent1 = world.new();
475+
const parent2 = world.new();
476+
477+
// Create specific relation queries (not wildcard)
478+
const queryParent1 = world.createQuery([relation(ChildOf, parent1), PositionId]);
479+
const queryParent2 = world.createQuery([relation(ChildOf, parent2), PositionId]);
480+
481+
// Create entities with different parents
482+
const entity1 = world.new();
483+
world.set(entity1, PositionId);
484+
world.set(entity1, relation(ChildOf, parent1));
485+
486+
const entity2 = world.new();
487+
world.set(entity2, PositionId);
488+
world.set(entity2, relation(ChildOf, parent2));
489+
490+
world.sync();
491+
492+
// Specific queries should find their respective entities
493+
expect(queryParent1.getEntities()).toContain(entity1);
494+
expect(queryParent1.getEntities()).not.toContain(entity2);
495+
496+
expect(queryParent2.getEntities()).not.toContain(entity1);
497+
expect(queryParent2.getEntities()).toContain(entity2);
498+
});
499+
500+
it("should support specific relation queries with exclusive dontFragment when target changes", () => {
501+
const world = new World();
502+
503+
const PositionId = component();
504+
const ChildOf = component({ dontFragment: true, exclusive: true });
505+
506+
const parent1 = world.new();
507+
const parent2 = world.new();
508+
const entity = world.new();
509+
world.set(entity, PositionId);
510+
511+
// Create specific queries for each parent
512+
const queryParent1 = world.createQuery([relation(ChildOf, parent1), PositionId]);
513+
const queryParent2 = world.createQuery([relation(ChildOf, parent2), PositionId]);
514+
515+
// Set relation to parent1
516+
world.set(entity, relation(ChildOf, parent1));
517+
world.sync();
518+
519+
expect(queryParent1.getEntities()).toContain(entity);
520+
expect(queryParent2.getEntities()).not.toContain(entity);
521+
522+
// Change to parent2
523+
world.set(entity, relation(ChildOf, parent2));
524+
world.sync();
525+
526+
expect(queryParent1.getEntities()).not.toContain(entity);
527+
expect(queryParent2.getEntities()).toContain(entity);
528+
529+
// Change back to parent1
530+
world.set(entity, relation(ChildOf, parent1));
531+
world.sync();
532+
533+
expect(queryParent1.getEntities()).toContain(entity);
534+
expect(queryParent2.getEntities()).not.toContain(entity);
535+
});
467536
});

src/query/filter.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { Archetype } from "../core/archetype";
22
import type { EntityId } from "../core/entity";
3-
import { getComponentIdFromRelationId, getDetailedIdType, isRelationId } from "../core/entity";
3+
import {
4+
getComponentIdFromRelationId,
5+
getDetailedIdType,
6+
isDontFragmentComponent,
7+
isRelationId,
8+
relation,
9+
} from "../core/entity";
410

511
/**
612
* Filter options for queries
@@ -32,8 +38,16 @@ export function matchesComponentTypes(archetype: Archetype, componentTypes: Enti
3238
const componentId = getComponentIdFromRelationId(archetypeType);
3339
return componentId === detailedType.componentId;
3440
});
41+
} else if (
42+
(detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
43+
detailedType.componentId !== undefined &&
44+
isDontFragmentComponent(detailedType.componentId)
45+
) {
46+
// For specific dontFragment relations, check if archetype has the wildcard marker
47+
const wildcardMarker = relation(detailedType.componentId, "*");
48+
return archetype.componentTypes.includes(wildcardMarker);
3549
} else {
36-
// For regular components, check direct inclusion
50+
// For regular components and non-dontFragment relations, check direct inclusion
3751
return archetype.componentTypes.includes(type);
3852
}
3953
});

src/query/query.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Archetype } from "../core/archetype";
22
import type { EntityId, WildcardRelationId } from "../core/entity";
3-
import { getDetailedIdType } from "../core/entity";
3+
import { getDetailedIdType, isDontFragmentComponent } from "../core/entity";
44
import type { ComponentTuple, ComponentType } from "../core/types";
55
import type { World } from "../core/world";
66
import { matchesComponentTypes, matchesFilter, type QueryFilter } from "./filter";
@@ -16,6 +16,8 @@ export class Query {
1616
private isDisposed = false;
1717
/** Cached wildcard component types for faster entity filtering */
1818
private wildcardTypes: WildcardRelationId<any>[];
19+
/** Cached specific dontFragment relation types that need entity-level filtering */
20+
private specificDontFragmentTypes: EntityId<any>[];
1921

2022
constructor(world: World, componentTypes: EntityId<any>[], filter: QueryFilter = {}) {
2123
this.world = world;
@@ -25,6 +27,15 @@ export class Query {
2527
this.wildcardTypes = this.componentTypes.filter(
2628
(ct) => getDetailedIdType(ct).type === "wildcard-relation",
2729
) as WildcardRelationId<any>[];
30+
// Pre-compute specific dontFragment relation types that need entity-level filtering
31+
this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
32+
const detailedType = getDetailedIdType(ct);
33+
return (
34+
(detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
35+
detailedType.componentId !== undefined &&
36+
isDontFragmentComponent(detailedType.componentId)
37+
);
38+
});
2839
this.updateCache();
2940
// Register with world for archetype updates
3041
world._registerQuery(this);
@@ -45,22 +56,23 @@ export class Query {
4556
getEntities(): EntityId[] {
4657
this.ensureNotDisposed();
4758

48-
// Fast path: no wildcard relations
49-
if (this.wildcardTypes.length === 0) {
59+
// Fast path: no wildcard relations and no specific dontFragment relations
60+
if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
5061
const result: EntityId[] = [];
5162
for (const archetype of this.cachedArchetypes) {
5263
result.push(...archetype.getEntities());
5364
}
5465
return result;
5566
}
5667

57-
// Slow path: need to filter entities that actually have wildcard relations
58-
// This is necessary for dontFragment components where an archetype can contain
59-
// entities with and without the relation
68+
// Slow path: need to filter entities that actually have the required relations
69+
// This is necessary for:
70+
// 1. Wildcard relations where an archetype can contain entities with/without the relation
71+
// 2. Specific dontFragment relations where the archetype only has the wildcard marker
6072
const result: EntityId[] = [];
6173
for (const archetype of this.cachedArchetypes) {
6274
for (const entity of archetype.getEntities()) {
63-
if (this.entityHasAllWildcards(archetype, entity)) {
75+
if (this.entityMatchesQuery(archetype, entity)) {
6476
result.push(entity);
6577
}
6678
}
@@ -69,15 +81,25 @@ export class Query {
6981
}
7082

7183
/**
72-
* Check if entity has all required wildcard relations
84+
* Check if entity matches all query requirements (wildcards and specific dontFragment relations)
7385
*/
74-
private entityHasAllWildcards(archetype: Archetype, entity: EntityId): boolean {
86+
private entityMatchesQuery(archetype: Archetype, entity: EntityId): boolean {
87+
// Check wildcard relations
7588
for (const wildcardType of this.wildcardTypes) {
7689
const relations = archetype.get(entity, wildcardType);
7790
if (!relations || relations.length === 0) {
7891
return false;
7992
}
8093
}
94+
95+
// Check specific dontFragment relations
96+
for (const specificType of this.specificDontFragmentTypes) {
97+
const result = archetype.getOptional(entity, specificType);
98+
if (result === undefined) {
99+
return false;
100+
}
101+
}
102+
81103
return true;
82104
}
83105

0 commit comments

Comments
 (0)