Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/plugins/driver-memory/src/memory-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,4 +749,45 @@ describe('InMemoryDriver', () => {
expect(second).not.toBe(first);
});
});

describe('aggregate() — QueryAST shape (analytics fallback path)', () => {
let driver: InMemoryDriver;
const tbl = 'expenses';

beforeEach(async () => {
driver = new InMemoryDriver({});
await driver.connect();
await driver.create(tbl, { id: '1', category: 'travel', amount: 100 });
await driver.create(tbl, { id: '2', category: 'travel', amount: 50 });
await driver.create(tbl, { id: '3', category: 'meals', amount: 30 });
});

it('accepts the engine AST ({groupBy, aggregations}) and returns grouped sums', async () => {
const rows = await driver.aggregate(tbl, {
object: tbl,
groupBy: ['category'],
aggregations: [{ function: 'sum', field: 'amount', alias: 'amount' }],
} as any);
const byCat = Object.fromEntries(rows.map((r: any) => [r.category, r.amount]));
expect(byCat).toEqual({ travel: 150, meals: 30 });
});

it('AST with no groupBy returns a single total row; where filters first', async () => {
const rows = await driver.aggregate(tbl, {
object: tbl,
where: { category: 'travel' },
aggregations: [{ function: 'sum', field: 'amount', alias: 'total' }, { function: 'count', field: '*', alias: 'count' }],
} as any);
expect(rows).toEqual([{ total: 150, count: 2 }]);
});

it('still accepts a real MongoDB pipeline array (Mingo passthrough)', async () => {
const rows = await driver.aggregate(tbl, [
{ $match: { category: 'travel' } },
{ $group: { _id: null, total: { $sum: '$amount' } } },
]);
expect((rows[0] as any).total).toBe(150);
});
});

});
30 changes: 27 additions & 3 deletions packages/plugins/driver-memory/src/memory-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,13 +625,37 @@
* { $group: { _id: null, avgPrice: { $avg: '$price' } } }
* ]);
*/
async aggregate(object: string, pipeline: Record<string, any>[], options?: DriverOptions): Promise<any[]> {
async aggregate(object: string, pipeline: Record<string, any>[] | QueryAST, options?: DriverOptions): Promise<any[]> {
// ObjectQL's engine calls driver.aggregate(object, AST) with the SAME
// QueryAST shape find() consumes ({ where, groupBy, aggregations }) — not a
// MongoDB pipeline. Passing that object into Mingo's Aggregator crashed
// with "this[#pipeline].map is not a function" (the analytics fallback path
// on in-memory environments). Detect the AST shape and serve it through the
// SAME filtering + performAggregation path find() uses; a real pipeline
// array keeps the Mingo behavior unchanged.
if (!Array.isArray(pipeline)) {
const query = pipeline as QueryAST;
this.logger.debug('Aggregate operation (QueryAST)', {
object,
groupBy: (query as any).groupBy,
aggregations: (query as any).aggregations?.length ?? 0,
});
let results = this.getTable(object).map((r) => ({ ...r }));
if (query.where) {
const mongoQuery = this.convertToMongoQuery(query.where);
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
results = new Query(mongoQuery).find(results).all() as Record<string, any>[];
}
}
return this.performAggregation(results, query);
}

this.logger.debug('Aggregate operation', { object, stageCount: pipeline.length });

const records = this.getTable(object).map(r => ({ ...r }));
const aggregator = new Aggregator(pipeline);
const results = aggregator.run(records);

this.logger.debug('Aggregate completed', { object, resultCount: results.length });
return results;
}
Expand Down Expand Up @@ -957,10 +981,10 @@
return nums.length > 0 ? sum / nums.length : null;
}

case 'min': {

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from library input.
// Handle comparable values
const valid = values.filter(v => v !== null && v !== undefined);
if (valid.length === 0) return null;

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from library input.
// Works for numbers and strings
return valid.reduce((min, v) => (v < min ? v : min), valid[0]);
}
Expand Down
Loading