Skip to content

Commit 5aea028

Browse files
authored
Merge pull request #99 from csprance/relationship-v2
## GECS v7.1.0 — Structural Relationship Queries **Relationship queries are now as fast as component queries.** No API changes required — your existing `with_relationship()` calls just got dramatically faster. ### What Changed `with_relationship()` previously worked as a post-filter: after selecting entities by component, it iterated every matched entity and linearly scanned their relationships array. This was O(N×M×K) where N = entities, M = relationships per entity, K = filters. In v7.1.0, each `(Relation, Target)` pair is baked into the archetype signature. Relationship queries now resolve through the same O(1) archetype bucket lookup that component queries use — no per-entity scanning. ### Performance (Godot 4.6, from benchmarks) **Exact relationship queries — constant time regardless of entity count:** | Scale | Relationship Query | Component Query (baseline) | |------:|-------------------:|---------------------------:| | 100 | 0.089 ms | 0.060 ms | | 1,000 | 0.092 ms | 0.061 ms | | 10,000 | 0.091 ms | 0.137 ms | Exact `with_relationship([Relationship.new(C_Type.new(), target)])` queries are **flat at ~0.09ms** whether you have 100 or 10,000 entities. That's within noise of a `with_all()` component query. **Wildcard relationship queries** (`Relationship.new(C_Type.new(), null)`) scale with the number of distinct archetypes matched, not total entities: | Scale | Wildcard Query | |------:|---------------:| | 100 | 0.214 ms | | 1,000 | 1.941 ms | | 10,000 | 21.274 ms | *(This benchmark is worst-case: every entity has a unique target, creating one archetype per entity. Real-world usage with shared targets will be much faster.)* ### What You Need to Do **Nothing.** The `with_relationship()` API is unchanged. Your existing code benefits automatically. ```gdscript # This is the same API — it's just fast now var children = world.query.with_relationship([ Relationship.new(C_ChildOf.new(), parent_entity) ]).execute() ``` Property-based relationship queries continue to work as post-filters (runtime values can't be archetype-keyed): ```gdscript # Still works — property queries remain post-filter var high_damage = world.query.with_relationship([ Relationship.new({C_Damage: {"amount": {"_gte": 50}}}, target) ]).execute() ``` ### Other Changes - **Legacy `relationship_entity_index` removed** — the old Dictionary-based index is gone, replaced entirely by the archetype system. Slightly lower memory footprint. - **Archetype explosion warning** — if you exceed 500 archetypes (possible with many unique relationship targets), a one-time debug warning fires so you can catch unintended combinatorial blowup early. - **Batch relationship transitions** — `add_relationships([r1, r2, r3])` produces exactly one archetype transition instead of N, keeping bulk operations fast. - **Freed-target cleanup** — when a target entity is removed from the world, all source relationships pointing to it are automatically cleaned up via the archetype index. ### Test Coverage 313 tests passing, zero regressions. 34 new tests added covering structural relationship storage, signature computation, archetype transitions, query integration, and property query preservation.
2 parents 4089af6 + fdb2163 commit 5aea028

60 files changed

Lines changed: 12986 additions & 1241 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
/android/
44
repomix-output.txt
55

6+
# Windows reserved device names (prevent accidental creation)
7+
nul
8+
69
reports/
710
.claude/settings.local.json
811

.planning/PROJECT.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# GECS Structural Relationships (v7.1.0)
2+
3+
## What This Is
4+
5+
GECS is a lightweight, performant Entity Component System (ECS) framework for Godot 4.x. This milestone adds FLECS-style structural relationship queries: each `(Relation, Target)` pair becomes part of the archetype signature so `with_relationship()` queries resolve via O(1) archetype bucket lookup instead of per-entity linear scanning.
6+
7+
## Core Value
8+
9+
Relationship queries must be as fast as component queries — both select pre-grouped archetype buckets, no per-entity iteration.
10+
11+
## Requirements
12+
13+
### Validated
14+
15+
- ✓ Archetype-based entity storage with FNV-1a signature hashing — existing
16+
- ✓ QueryBuilder with `with_all`, `with_any`, `with_none`, `with_group` — existing
17+
- ✓ Relationship system: typed `(relation, target)` pairs stored on entities — existing
18+
-`with_relationship()` query filter (currently post-filter, slow) — existing
19+
- ✓ Wildcard null-target relationship matching — existing
20+
- ✓ Property-based relationship queries via `ComponentQueryMatcher` — existing
21+
- ✓ CommandBuffer for safe structural mutations during iteration — existing
22+
- ✓ Observer/reactive system for component lifecycle events — existing
23+
- ✓ Archetype add/remove edge graph for O(1) archetype transitions — existing
24+
- ✓ Query archetype cache (FNV-1a keyed, invalidated on structural changes) — existing
25+
- ✓ Network sync addon (`gecs_network`) — existing
26+
- ✓ Serialization via `GECSIO` — existing
27+
- ✓ Each unique `(Relation, Target)` pair included in archetype signature
28+
-`entity.add_relationship()` moves entity to new archetype (structural transition)
29+
-`entity.remove_relationship()` moves entity back (structural transition)
30+
-`with_relationship()` exact-pair queries resolve via archetype cache lookup
31+
- ✓ Wildcard relation queries use a relation-type index bucket
32+
- ✓ Archetype query cache key includes structural relationship pairs
33+
- ✓ Cache invalidation triggers on relationship add/remove
34+
- ✓ New tests cover the structural archetype query path for relationships
35+
36+
### Active
37+
38+
- [ ] Property-based relationship queries remain as post-filter applied after archetype selection
39+
- [ ] All existing relationship tests pass unchanged as a formally tracked phase-5 deliverable
40+
- [ ] Perf benchmarks demonstrate O(1) relationship query parity with component queries
41+
- [ ] Ships as v7.1.0 — no public API breaks on World, Entity, QueryBuilder
42+
43+
### Out of Scope
44+
45+
- Changing the `with_relationship()` call site API — internal implementation only
46+
- Making property-based relationship queries structural — runtime values can't be archetype-keyed
47+
- Breaking any public API surface on World, Entity, QueryBuilder, System, Observer
48+
- Network sync changes — not affected by this milestone
49+
50+
## Context
51+
52+
Current bottleneck: `QueryBuilder` applies `with_relationship()` as a post-filter — it iterates every entity in the structural result set and calls `entity.has_relationship()`, which linearly scans `entity.relationships: Array[Relationship]`. This is O(N×M×K) where N = matched entities, M = relationships per entity, K = relationship filters.
53+
54+
FLECS solves this by treating `(ChildOf, parent_entity)` as a first-class component slot in the archetype signature. The archetype hash includes relationship pairs so the query just selects matching archetype buckets — same O(1) path as component queries.
55+
56+
The existing `relationship_entity_index: Dictionary` in World (`relation.resource_path → Array[Entity]`) is built but not used for queries. It will be replaced or extended to support the new structural approach.
57+
58+
Entity relationships support three target types: Entity instance (identity), Component instance (type-matched), or null (wildcard). The archetype key must handle all three.
59+
60+
## Constraints
61+
62+
- **Compatibility**: No breaking changes to the public API (World, Entity, QueryBuilder, System, Observer, Relationship constructors) — v7.1.0 semver
63+
- **Godot 4.x**: GDScript only, no C++ extensions
64+
- **Test coverage**: All changes must have corresponding gdUnit4 tests in `addons/gecs/tests/`
65+
- **Perf validation**: Perf benchmarks in `addons/gecs/tests/performance/` must show structural relationship queries matching component query performance
66+
67+
## Key Decisions
68+
69+
| Decision | Rationale | Outcome |
70+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
71+
| Each (Relation, Target) pair = unique archetype slot | Full FLECS fidelity; enables per-target structural queries; entity with (Damage,e1) AND (Damage,e2) lives in archetype including both pairs | Validated in phases 1-4 |
72+
| Property queries stay as post-filter | Runtime property values can't be hashed into archetype keys | Validated; remains a phase 5 compatibility focus |
73+
| Keep `with_relationship()` API unchanged | No user code churn; just make it fast internally | Validated |
74+
| Wildcard (null target) uses relation-type index | Separate bucket from exact-pair bucket; covers the common "has any X relationship" pattern fast | Validated |
75+
76+
---
77+
78+
_Last updated: 2026-03-22 after Phase 04 verification_

.planning/REQUIREMENTS.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Requirements: GECS v7.1.0 — Structural Relationships
2+
3+
**Defined:** 2026-03-18
4+
**Core Value:** Relationship queries must be as fast as component queries — both select pre-grouped archetype buckets, no per-entity iteration.
5+
6+
## v1 Requirements
7+
8+
Requirements for v7.1.0 release. Each maps to roadmap phases.
9+
10+
### Archetype Pairs
11+
12+
- [ ] **ARCH-01**: Each unique `(Relation, Target)` pair is encoded as a slot key and stored in the archetype's `component_types` array alongside component resource paths
13+
- [ ] **ARCH-02**: Relationship slot keys use `rel://<relation_resource_path>::<target_key>` string format in `component_types` (uniform with existing component path infrastructure)
14+
- [ ] **ARCH-03**: Archetype does NOT create SoA columns for `rel://` prefixed slot keys (relationship data remains on `entity.relationships`)
15+
- [ ] **ARCH-04**: Archetype exposes a `relationship_types` array for efficient pair subset iteration
16+
- [ ] **ARCH-05**: Archetype `matches_relationship_query()` method performs structural pair matching
17+
18+
### Signature & Index
19+
20+
- [ ] **SIGX-01**: `World._calculate_entity_signature()` includes relationship slot keys in the entity's archetype signature hash
21+
- [ ] **SIGX-02**: `QueryCacheKey.build()` encodes relationship pairs as `(relation_id, target_id)` integer pairs (not flattened — preserves pair structure to prevent hash collisions)
22+
- [ ] **SIGX-03**: World maintains a `_relation_type_archetype_index: Dictionary` mapping `relation.resource_path → Array[Archetype]` for O(1) wildcard queries
23+
- [ ] **SIGX-04**: `_relation_type_archetype_index` is updated when archetypes are created and destroyed
24+
25+
### Structural Transitions
26+
27+
- [ ] **TRAN-01**: `entity.add_relationship()` triggers an archetype transition (entity moves to new archetype including the pair slot key)
28+
- [ ] **TRAN-02**: `entity.remove_relationship()` triggers an archetype transition (entity moves to archetype without the pair slot key)
29+
- [ ] **TRAN-03**: `entity.add_relationships()` batches to a single archetype transition (not N sequential transitions) to prevent cache thrash
30+
- [ ] **TRAN-04**: Cache invalidation fires on relationship add/remove (currently deliberately disabled — must be re-enabled for structural correctness)
31+
- [ ] **TRAN-05**: When a target entity is removed from the World, all source entities holding `(Relation, freed_target)` relationships are cleaned up (REMOVE policy — relationship is deleted when target is deleted, same as FLECS default)
32+
33+
### Query Integration
34+
35+
- [ ] **QURY-01**: `QueryBuilder.get_cache_key()` passes relationship pairs to `QueryCacheKey.build()` (currently always passes empty arrays — must be fixed)
36+
- [ ] **QURY-02**: `with_relationship()` with exact `(Relation, Target)` resolves via archetype cache lookup, not per-entity scan
37+
- [ ] **QURY-03**: `with_relationship()` with null target (wildcard) resolves via `_relation_type_archetype_index`, not per-entity scan
38+
- [ ] **QURY-04**: `without_relationship()` resolves structurally via archetype exclusion
39+
- [ ] **QURY-05**: `System._query_has_non_structural_filters()` does NOT flag exact type-match relationships as non-structural (only property-query relationships are non-structural)
40+
- [ ] **QURY-06**: Archetype subsumption: entity-instance target `e_pizza` matches a query using script-archetype target `GecsFood` (via wildcard + post-filter strategy)
41+
42+
### Property Query Preservation
43+
44+
- [ ] **PROP-01**: Property-based relationship queries (`Relationship.new({C_Damage: {'amount': {'_gte': 50}}}, target)`) remain as post-filters applied after archetype narrowing
45+
- [ ] **PROP-02**: `_query_has_non_structural_filters()` still returns true for property-query relationships
46+
- [ ] **PROP-03**: All 20+ existing relationship tests in `test_relationships.gd` pass unchanged
47+
48+
### Perf Validation & Cleanup
49+
50+
- [ ] **PERF-01**: Performance benchmarks demonstrate relationship query time parity with equivalent component queries at scales of 100, 1000, 10000 entities
51+
- [ ] **PERF-02**: `relationship_entity_index` (legacy partial index in World) is removed or deprecated in favor of `_relation_type_archetype_index`
52+
- [ ] **PERF-03**: Archetype count monitoring added to debug mode output (detect archetype explosion in development)
53+
54+
## v2 Requirements
55+
56+
Deferred to v7.2.0 or later.
57+
58+
### Relationship Traits
59+
60+
- **TRAIT-01**: Exclusive relationship trait — entity can only hold one target per relation type (enforced on `add_relationship()`)
61+
- **TRAIT-02**: Symmetric relationship trait — adding `(R, B)` to A auto-adds `(R, A)` to B
62+
63+
### Observer Integration
64+
65+
- **OBS-01**: Observer `on_relationship_added` callback fires when a relationship pair is structurally added
66+
- **OBS-02**: Observer `on_relationship_removed` callback fires when a relationship pair is structurally removed
67+
68+
### Advanced Queries
69+
70+
- **ADVQ-01**: `with_any_relationship()` filter — OR semantics across multiple relationship patterns
71+
- **ADVQ-02**: Dual-index registration for archetype subsumption (Option A) as a performance follow-up if wildcard+post-filter proves too slow
72+
73+
## Out of Scope
74+
75+
| Feature | Reason |
76+
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
77+
| Property-based relationship queries becoming structural | Runtime values can't be archetype-keyed by definition |
78+
| Transitive relationship queries / graph traversal at query time | Defeats O(1) archetype lookup; anti-feature for GDScript performance |
79+
| Traversal / `up()` queries (FLECS pattern) | Same anti-feature rationale |
80+
| OnDelete cascade policy (delete parent cascades to children) | v2+ complexity; v7.1.0 ships REMOVE policy only |
81+
| Pair data access from archetype SoA columns | Relationship data stays on entity.relationships; archetype tracks identity only |
82+
| Breaking any public API on World, Entity, QueryBuilder, System, Observer | v7.1.0 semver — no public API breaks |
83+
| Network sync changes | Not affected by this milestone |
84+
85+
## Traceability
86+
87+
Updated during roadmap creation.
88+
89+
| Requirement | Phase | Status |
90+
| ----------- | ------- | --------- |
91+
| ARCH-01 | Phase 1 | Completed |
92+
| ARCH-02 | Phase 1 | Completed |
93+
| ARCH-03 | Phase 1 | Completed |
94+
| ARCH-04 | Phase 1 | Completed |
95+
| ARCH-05 | Phase 1 | Completed |
96+
| SIGX-01 | Phase 2 | Completed |
97+
| SIGX-02 | Phase 2 | Completed |
98+
| SIGX-03 | Phase 2 | Completed |
99+
| SIGX-04 | Phase 2 | Completed |
100+
| TRAN-01 | Phase 3 | Completed |
101+
| TRAN-02 | Phase 3 | Completed |
102+
| TRAN-03 | Phase 3 | Completed |
103+
| TRAN-04 | Phase 3 | Completed |
104+
| TRAN-05 | Phase 3 | Completed |
105+
| QURY-01 | Phase 4 | Completed |
106+
| QURY-02 | Phase 4 | Completed |
107+
| QURY-03 | Phase 4 | Completed |
108+
| QURY-04 | Phase 4 | Completed |
109+
| QURY-05 | Phase 4 | Completed |
110+
| QURY-06 | Phase 4 | Completed |
111+
| PROP-01 | Phase 5 | Pending |
112+
| PROP-02 | Phase 5 | Pending |
113+
| PROP-03 | Phase 5 | Pending |
114+
| PERF-01 | Phase 6 | Pending |
115+
| PERF-02 | Phase 6 | Pending |
116+
| PERF-03 | Phase 6 | Pending |
117+
118+
**Coverage:**
119+
120+
- v1 requirements: 26 total
121+
- Mapped to phases: 26
122+
- Unmapped: 0 ✓
123+
124+
---
125+
126+
_Requirements defined: 2026-03-18_
127+
_Last updated: 2026-03-22 after Phase 04 verification_

0 commit comments

Comments
 (0)