Skip to content

Commit 922baa8

Browse files
committed
feat(driver-memory): implement aggregation and grouping logic in InMemoryDriver
1 parent 75a509c commit 922baa8

1 file changed

Lines changed: 118 additions & 1 deletion

File tree

packages/plugins/driver-memory/src/memory-driver.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)