@@ -38,7 +38,7 @@ export class InMemoryDriver implements DriverInterface {
3838
3939 // Query Operations
4040 queryFilters : true , // Implemented via memory-matcher
41- queryAggregations : false , // TODO: Not implemented - count() only returns total
41+ queryAggregations : true , // Implemented
4242 querySorting : true , // Implemented via JS sort
4343 queryPagination : true , // Implemented
4444 queryWindowFunctions : false , // TODO: Not implemented
@@ -109,6 +109,11 @@ export class InMemoryDriver implements DriverInterface {
109109 results = results . filter ( record => match ( record , query . where ) ) ;
110110 }
111111
112+ // 1.5 Aggregation & Grouping
113+ if ( query . groupBy || ( query . aggregations && query . aggregations . length > 0 ) ) {
114+ results = this . performAggregation ( results , query ) ;
115+ }
116+
112117 // 2. Sort
113118 if ( query . orderBy ) {
114119 // Normalize sort to array
@@ -359,6 +364,118 @@ export class InMemoryDriver implements DriverInterface {
359364 async commit ( ) { /* No-op */ }
360365 async rollback ( ) { /* No-op */ }
361366
367+ // ===================================
368+ // Aggregation Logic
369+ // ===================================
370+
371+ private performAggregation ( records : any [ ] , query : QueryInput ) : any [ ] {
372+ const { groupBy, aggregations } = query ;
373+ const groups : Map < string , any [ ] > = new Map ( ) ;
374+
375+ // 1. Group records
376+ if ( groupBy && groupBy . length > 0 ) {
377+ for ( const record of records ) {
378+ // Create a composite key from group values
379+ const keyParts = groupBy . map ( field => {
380+ const val = getValueByPath ( record , field ) ;
381+ return val === undefined || val === null ? 'null' : String ( val ) ;
382+ } ) ;
383+ const key = JSON . stringify ( keyParts ) ;
384+
385+ if ( ! groups . has ( key ) ) {
386+ groups . set ( key , [ ] ) ;
387+ }
388+ groups . get ( key ) ! . push ( record ) ;
389+ }
390+ } else {
391+ // No grouping -> Single group containing all records
392+ // If aggregation is requested without group by, it runs on whole set (even if empty)
393+ if ( aggregations && aggregations . length > 0 ) {
394+ groups . set ( 'all' , records ) ;
395+ } else {
396+ // Should not be here if performAggregation called correctly
397+ groups . set ( 'all' , records ) ;
398+ }
399+ }
400+
401+ // 2. Compute aggregates for each group
402+ const resultRows : any [ ] = [ ] ;
403+
404+ for ( const [ key , groupRecords ] of groups . entries ( ) ) {
405+ const row : any = { } ;
406+
407+ // A. Add Group fields to row (if groupBy exists)
408+ if ( groupBy && groupBy . length > 0 ) {
409+ if ( groupRecords . length > 0 ) {
410+ const firstRecord = groupRecords [ 0 ] ;
411+ for ( const field of groupBy ) {
412+ this . setValueByPath ( row , field , getValueByPath ( firstRecord , field ) ) ;
413+ }
414+ }
415+ }
416+
417+ // B. Compute Aggregations
418+ if ( aggregations ) {
419+ for ( const agg of aggregations ) {
420+ const value = this . computeAggregate ( groupRecords , agg ) ;
421+ row [ agg . alias ] = value ;
422+ }
423+ }
424+
425+ resultRows . push ( row ) ;
426+ }
427+
428+ return resultRows ;
429+ }
430+
431+ private computeAggregate ( records : any [ ] , agg : any ) : any {
432+ const { function : func , field } = agg ;
433+
434+ const values = field ? records . map ( r => getValueByPath ( r , field ) ) : [ ] ;
435+
436+ switch ( func ) {
437+ case 'count' :
438+ if ( ! field || field === '*' ) return records . length ;
439+ return values . filter ( v => v !== null && v !== undefined ) . length ;
440+
441+ case 'sum' :
442+ case 'avg' : {
443+ const nums = values . filter ( v => typeof v === 'number' ) ;
444+ const sum = nums . reduce ( ( a , b ) => a + b , 0 ) ;
445+ if ( func === 'sum' ) return sum ;
446+ return nums . length > 0 ? sum / nums . length : null ;
447+ }
448+
449+ case 'min' : {
450+ // Handle comparable values
451+ const valid = values . filter ( v => v !== null && v !== undefined ) ;
452+ if ( valid . length === 0 ) return null ;
453+ // Works for numbers and strings
454+ return valid . reduce ( ( min , v ) => ( v < min ? v : min ) , valid [ 0 ] ) ;
455+ }
456+
457+ case 'max' : {
458+ const valid = values . filter ( v => v !== null && v !== undefined ) ;
459+ if ( valid . length === 0 ) return null ;
460+ return valid . reduce ( ( max , v ) => ( v > max ? v : max ) , valid [ 0 ] ) ;
461+ }
462+
463+ default :
464+ return null ;
465+ }
466+ }
467+
468+ private setValueByPath ( obj : any , path : string , value : any ) {
469+ const parts = path . split ( '.' ) ;
470+ let current = obj ;
471+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
472+ const part = parts [ i ] ;
473+ if ( ! current [ part ] ) current [ part ] = { } ;
474+ current = current [ part ] ;
475+ }
476+ current [ parts [ parts . length - 1 ] ] = value ;
477+ }
478+
362479 // ===================================
363480 // Helpers
364481 // ===================================
0 commit comments