@@ -13,36 +13,11 @@ import (
1313// We use expr lang for the where clause to filter the entities, please refer to its documentation
1414// for more details: https://expr-lang.org/docs/getting-started.
1515type SearchParam struct {
16- Find []string // List of component names to search for
17- Match SearchMatch // A match type to use for the search
18- Where string // Optional expr language string to filter the results.
19- }
20-
21- // validateAndGetFilter validates the search parameters and returns an expr VM program compiled
22- // from the where clause.
23- func (s * SearchParam ) validateAndGetFilter () (* vm.Program , error ) {
24- if len (s .Find ) == 0 {
25- return nil , eris .New ("component list cannot be empty" )
26- }
27-
28- if s .Match != MatchExact && s .Match != MatchContains {
29- return nil , eris .Errorf ("invalid `match` value: must be either '%s' or '%s'" , MatchExact , MatchContains )
30- }
31-
32- var filter * vm.Program
33-
34- // If no expression is provided, return a nil program
35- if len (s .Where ) == 0 {
36- return filter , nil
37- }
38-
39- // Compile the expression and check that the return type is boolean.
40- filter , err := expr .Compile (s .Where , expr .AsBool ())
41- if err != nil {
42- return nil , eris .Wrap (err , "failed to parse where clause" )
43- }
44-
45- return filter , nil
16+ Find []string // List of component names to search for. Must be empty when Match is MatchAll.
17+ Match SearchMatch // A match type to use for the search.
18+ Where string // Optional expr language string to filter the results.
19+ Limit uint32 // Maximum number of results to return (default: unlimited, 0 = unlimited)
20+ Offset uint32 // Number of results to skip before returning (default: 0)
4621}
4722
4823// SearchMatch is the type of match to use for the search.
@@ -54,9 +29,16 @@ const (
5429 // MatchContains matches entities that contains the specified components, but may have other
5530 // components as well.
5631 MatchContains SearchMatch = "contains"
32+ // MatchAll matches all entities regardless of components. Find must be empty when using this.
33+ MatchAll SearchMatch = "all"
5734)
5835
36+ // DefaultQueryLimit is the default maximum number of results returned when Limit is 0 (unlimited).
37+ const DefaultQueryLimit = ^ uint32 (0 ) // Max uint32 (4294967295)
38+
5939// NewSearch returns a map of entities that match the given search parameters.
40+ //
41+ //nolint:gocognit // Complexity from sequential filtering (where, offset, limit) kept together for readability.
6042func (w * World ) NewSearch (params SearchParam ) ([]map [string ]any , error ) {
6143 filter , err := params .validateAndGetFilter ()
6244 if err != nil {
@@ -68,60 +50,99 @@ func (w *World) NewSearch(params SearchParam) ([]map[string]any, error) {
6850 return nil , eris .Wrap (err , "failed to get archetypes from components" )
6951 }
7052
53+ limit := params .Limit
54+ if limit == 0 {
55+ limit = DefaultQueryLimit
56+ }
57+
7158 results := make ([]map [string ]any , 0 )
59+ skipped := uint32 (0 )
60+ collected := uint32 (0 )
61+
7262 for _ , id := range archetypeIDs {
73- // Makes a copy of the arch.
7463 arch := w .state .archetypes [id ]
7564
7665 for eid , components := range archIter (arch ) {
77- // Create the payload map.
78- result := make (map [string ]any )
79- // We have to cast id from EntityID to int here or else we can't query the data because for some
80- // reason expr can't compare EntityID with integers in the expression.
81- result ["_id" ] = uint32 (eid )
82-
83- for _ , component := range components {
84- result [component .Name ()] = component
66+ result := buildEntityResult (eid , components )
67+
68+ // Apply Where filter if present
69+ if filter != nil {
70+ matches , err := matchesFilter (filter , result )
71+ if err != nil {
72+ return nil , err
73+ }
74+ if ! matches {
75+ continue // Skip this entity
76+ }
8577 }
8678
87- // If there's no filter, include all entities.
88- if filter == nil {
89- results = append ( results , result )
79+ // Apply offset: skip first N matching entities
80+ if skipped < params . Offset {
81+ skipped ++
9082 continue
9183 }
9284
93- // Run the filter expression. We set the entity map as the environment for `Run` so the vm
94- // program has access to the entity data to filter.
95- output , innerErr := expr .Run (filter , result )
96- if innerErr != nil {
97- return nil , eris .Wrap (innerErr , "failed to run filter expression" )
98- }
99-
100- isMatchFilter , ok := output .(bool )
101- // Because we compile the expr once without passing in the environment, as it's only available
102- // while iterating, expr.Compile can't fully check if the expression returns a bool,x
103- // especially when we filter for a struct field e.g. health.hp > 200, expr can't determine the
104- // type of health.hp during compilation.
105- if ! ok {
106- return nil , eris .New ("invalid where clause" )
107- }
85+ // Add to results
86+ results = append (results , result )
87+ collected ++
10888
109- if isMatchFilter {
110- results = append (results , result )
89+ // Early termination: stop when limit is reached
90+ if collected >= limit {
91+ return results , nil
11192 }
11293 }
11394 }
11495
11596 return results , nil
11697}
11798
99+ // validateAndGetFilter validates the search parameters and returns an expr VM program compiled
100+ // from the where clause.
101+ func (s * SearchParam ) validateAndGetFilter () (* vm.Program , error ) {
102+ // Validate Match and Find relationship
103+ if s .Match == MatchAll {
104+ if len (s .Find ) > 0 {
105+ return nil , eris .New ("find must be empty when match is 'all'" )
106+ }
107+ } else {
108+ if len (s .Find ) == 0 {
109+ return nil , eris .New ("find must not be empty when match is not 'all'" )
110+ }
111+ if s .Match != MatchExact && s .Match != MatchContains {
112+ return nil , eris .Errorf ("invalid `match` value: must be either '%s' or '%s'" , MatchExact , MatchContains )
113+ }
114+ }
115+
116+ var filter * vm.Program
117+
118+ // If no expression is provided, return a nil program
119+ if len (s .Where ) == 0 {
120+ return filter , nil
121+ }
122+
123+ // Compile the expression and check that the return type is boolean.
124+ filter , err := expr .Compile (s .Where , expr .AsBool ())
125+ if err != nil {
126+ return nil , eris .Wrap (err , "failed to parse where clause" )
127+ }
128+
129+ return filter , nil
130+ }
131+
118132// findMatchingArchetypes returns the archetypes that match the given components and match type.
119133func findMatchingArchetypes (w * World , compNames []string , match SearchMatch ) ([]archetypeID , error ) {
120- if len (compNames ) == 0 {
121- return nil , eris .New ("component list cannot be empty" )
134+ ws := w .state
135+
136+ // If match is MatchAll, return all archetype IDs
137+ if match == MatchAll {
138+ archIDs := make ([]archetypeID , 0 , len (ws .archetypes ))
139+ for id := range ws .archetypes {
140+ archIDs = append (archIDs , id )
141+ }
142+ return archIDs , nil
122143 }
123144
124- ws := w . state
145+ // Build component bitmap from names
125146 component := bitmap.Bitmap {}
126147 for _ , name := range compNames {
127148 id , exists := ws .components .catalog [name ]
@@ -131,6 +152,7 @@ func findMatchingArchetypes(w *World, compNames []string, match SearchMatch) ([]
131152 component .Set (id )
132153 }
133154
155+ // Find matching archetypes based on match type
134156 var archIDs []int
135157 switch match {
136158 case MatchExact :
@@ -140,10 +162,49 @@ func findMatchingArchetypes(w *World, compNames []string, match SearchMatch) ([]
140162 }
141163 case MatchContains :
142164 archIDs = ws .archContains (component )
165+ case MatchAll :
166+ // This case should never be reached as MatchAll is handled earlier in the function
167+ // but included for exhaustive switch coverage
168+ return nil , eris .New ("MatchAll should be handled before this switch" )
143169 }
144170 return archIDs , nil
145171}
146172
173+ // buildEntityResult creates a result map from an entity ID and its components.
174+ func buildEntityResult (eid EntityID , components []Component ) map [string ]any {
175+ result := make (map [string ]any )
176+ // We have to cast id from EntityID to int here or else we can't query the data because for some
177+ // reason expr can't compare EntityID with integers in the expression.
178+ result ["_id" ] = uint32 (eid )
179+
180+ for _ , component := range components {
181+ result [component .Name ()] = component
182+ }
183+
184+ return result
185+ }
186+
187+ // matchesFilter checks if an entity matches the filter expression.
188+ func matchesFilter (filter * vm.Program , result map [string ]any ) (bool , error ) {
189+ // Run the filter expression. We set the entity map as the environment for `Run` so the vm
190+ // program has access to the entity data to filter.
191+ output , err := expr .Run (filter , result )
192+ if err != nil {
193+ return false , eris .Wrap (err , "failed to run filter expression" )
194+ }
195+
196+ isMatchFilter , ok := output .(bool )
197+ // Because we compile the expr once without passing in the environment, as it's only available
198+ // while iterating, expr.Compile can't fully check if the expression returns a bool,
199+ // especially when we filter for a struct field e.g. health.hp > 200, expr can't determine the
200+ // type of health.hp during compilation.
201+ if ! ok {
202+ return false , eris .New ("invalid where clause" )
203+ }
204+
205+ return isMatchFilter , nil
206+ }
207+
147208// archIter returns an iterator of the archetypes entities and its components.
148209func archIter (a * archetype ) iter.Seq2 [EntityID , []Component ] {
149210 return func (yield func (EntityID , []Component ) bool ) {
0 commit comments