@@ -8,13 +8,18 @@ module CQL
88 class QueryBuilder (T )
99 @query : CQL ::Query
1010 @model_class : T .class
11+ @preload_specs : Array (Symbol ) = [] of Symbol
1112
1213 # Initialize a new QueryBuilder with the given query
1314 # - **@param** query [CQL::Query] The underlying query object
1415 # - **@param** model_class [T.class] The model class
1516 def initialize (@query : CQL ::Query , @model_class : T .class = T )
1617 end
1718
19+ # Initialize with preload specs (used internally for cloning)
20+ protected def initialize (@query : CQL ::Query , @model_class : T .class, @preload_specs : Array (Symbol ))
21+ end
22+
1823 # Get the model class
1924 def model_class : T .class
2025 @model_class
@@ -48,7 +53,7 @@ module CQL
4853 # Clone the current QueryBuilder and apply modifications
4954 # - **@return** [QueryBuilder(T)] A new query builder instance
5055 private def clone_builder : QueryBuilder (T )
51- QueryBuilder (T ).new(@query .clone, @model_class )
56+ QueryBuilder (T ).new(@query .clone, @model_class , @preload_specs .dup )
5257 end
5358
5459 # Add columns to select
@@ -266,12 +271,64 @@ module CQL
266271 QueryBuilder (T ).new(merged_query, @model_class )
267272 end
268273
274+ # Add associations to preload (eager load) to avoid N+1 queries.
275+ # Preloaded associations are fetched in separate batch queries after
276+ # the main query executes, then injected into the parent records.
277+ # - **@param** associations [Symbol*] The association names to preload
278+ # - **@return** [QueryBuilder(T)] A new query builder with preload configured
279+ #
280+ # **Example**
281+ # ```
282+ # User.preload(:posts).all # Preload single association
283+ # User.preload(:posts, :comments).all # Preload multiple associations
284+ # User.where(active: true).preload(:posts).all # Chain with where
285+ # ```
286+ def preload (* associations : Symbol ) : QueryBuilder (T )
287+ builder = clone_builder
288+ associations.each { |assoc | builder.@preload_specs << assoc }
289+ builder
290+ end
291+
269292 # Execute the query and return all results
293+ # When preloads are configured, automatically applies them to the results.
270294 # - **@return** [Array(T)] Array of model instances
271- def all (as as_kind = T )
295+ def all : Array (T )
296+ records = @query .all(T )
297+ apply_preloads(records) if @preload_specs .any?
298+ records
299+ end
300+
301+ # Execute the query and return all results as a custom type
302+ # Note: Preloading is not applied when using a custom type.
303+ # - **@param** as_kind [Class] The type to deserialize results as
304+ # - **@return** [Array] Array of instances of the specified type
305+ def all (as as_kind)
272306 @query .all(as_kind)
273307 end
274308
309+ # Apply preloads to the fetched records
310+ private def apply_preloads (records : Array (T )) : Nil
311+ return if records.empty?
312+
313+ # Collect parent IDs for batch loading
314+ parent_ids = records.compact_map(& .id)
315+ return if parent_ids.empty?
316+
317+ # For each association to preload, call the class method generated by has_many
318+ @preload_specs .each do |assoc_name |
319+ {% begin % }
320+ case assoc_name
321+ {% for method in @type .type_vars[0 ].class.methods % }
322+ {% if method.name.starts_with?(" _preload_" ) % }
323+ when {{method.name.stringify.gsub(/^_preload_/ , " " ).id.symbolize}}
324+ T .{{method.name}}(records, parent_ids)
325+ {% end % }
326+ {% end % }
327+ end
328+ {% end % }
329+ end
330+ end
331+
275332 # Execute the query and return the first result
276333 # - **@return** [T?] First model instance or nil
277334 def first (as as_kind = T )
0 commit comments