Skip to content

Commit 649e56b

Browse files
os-zhuangclaude
andauthored
fix(driver-memory): accept the engine QueryAST in aggregate() (analytics fallback) (#1703)
ObjectQL's engine calls driver.aggregate(object, AST) with the same { where, groupBy, aggregations } shape find() consumes — but the in-memory driver's aggregate() only understood MongoDB pipeline arrays, so the analytics RAW_SQL_UNSUPPORTED fallback (framework#1701) crashed with "this[#pipeline].map is not a function" on in-memory environments — the last broken link in dataset queries on staging tenants. aggregate() now detects the AST shape and serves it through the SAME where-filter + performAggregation path find() uses (count/sum/avg/min/max, multi-field groupBy, aliases); a real pipeline array keeps the Mingo behavior unchanged. driver-memory: 68 tests green (3 new: grouped sums via AST, where+no-groupBy single-row totals, pipeline-array passthrough). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c7c51c4 commit 649e56b

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,4 +749,45 @@ describe('InMemoryDriver', () => {
749749
expect(second).not.toBe(first);
750750
});
751751
});
752+
753+
describe('aggregate() — QueryAST shape (analytics fallback path)', () => {
754+
let driver: InMemoryDriver;
755+
const tbl = 'expenses';
756+
757+
beforeEach(async () => {
758+
driver = new InMemoryDriver({});
759+
await driver.connect();
760+
await driver.create(tbl, { id: '1', category: 'travel', amount: 100 });
761+
await driver.create(tbl, { id: '2', category: 'travel', amount: 50 });
762+
await driver.create(tbl, { id: '3', category: 'meals', amount: 30 });
763+
});
764+
765+
it('accepts the engine AST ({groupBy, aggregations}) and returns grouped sums', async () => {
766+
const rows = await driver.aggregate(tbl, {
767+
object: tbl,
768+
groupBy: ['category'],
769+
aggregations: [{ function: 'sum', field: 'amount', alias: 'amount' }],
770+
} as any);
771+
const byCat = Object.fromEntries(rows.map((r: any) => [r.category, r.amount]));
772+
expect(byCat).toEqual({ travel: 150, meals: 30 });
773+
});
774+
775+
it('AST with no groupBy returns a single total row; where filters first', async () => {
776+
const rows = await driver.aggregate(tbl, {
777+
object: tbl,
778+
where: { category: 'travel' },
779+
aggregations: [{ function: 'sum', field: 'amount', alias: 'total' }, { function: 'count', field: '*', alias: 'count' }],
780+
} as any);
781+
expect(rows).toEqual([{ total: 150, count: 2 }]);
782+
});
783+
784+
it('still accepts a real MongoDB pipeline array (Mingo passthrough)', async () => {
785+
const rows = await driver.aggregate(tbl, [
786+
{ $match: { category: 'travel' } },
787+
{ $group: { _id: null, total: { $sum: '$amount' } } },
788+
]);
789+
expect((rows[0] as any).total).toBe(150);
790+
});
791+
});
792+
752793
});

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,13 +625,37 @@ export class InMemoryDriver implements IDataDriver {
625625
* { $group: { _id: null, avgPrice: { $avg: '$price' } } }
626626
* ]);
627627
*/
628-
async aggregate(object: string, pipeline: Record<string, any>[], options?: DriverOptions): Promise<any[]> {
628+
async aggregate(object: string, pipeline: Record<string, any>[] | QueryAST, options?: DriverOptions): Promise<any[]> {
629+
// ObjectQL's engine calls driver.aggregate(object, AST) with the SAME
630+
// QueryAST shape find() consumes ({ where, groupBy, aggregations }) — not a
631+
// MongoDB pipeline. Passing that object into Mingo's Aggregator crashed
632+
// with "this[#pipeline].map is not a function" (the analytics fallback path
633+
// on in-memory environments). Detect the AST shape and serve it through the
634+
// SAME filtering + performAggregation path find() uses; a real pipeline
635+
// array keeps the Mingo behavior unchanged.
636+
if (!Array.isArray(pipeline)) {
637+
const query = pipeline as QueryAST;
638+
this.logger.debug('Aggregate operation (QueryAST)', {
639+
object,
640+
groupBy: (query as any).groupBy,
641+
aggregations: (query as any).aggregations?.length ?? 0,
642+
});
643+
let results = this.getTable(object).map((r) => ({ ...r }));
644+
if (query.where) {
645+
const mongoQuery = this.convertToMongoQuery(query.where);
646+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
647+
results = new Query(mongoQuery).find(results).all() as Record<string, any>[];
648+
}
649+
}
650+
return this.performAggregation(results, query);
651+
}
652+
629653
this.logger.debug('Aggregate operation', { object, stageCount: pipeline.length });
630-
654+
631655
const records = this.getTable(object).map(r => ({ ...r }));
632656
const aggregator = new Aggregator(pipeline);
633657
const results = aggregator.run(records);
634-
658+
635659
this.logger.debug('Aggregate completed', { object, resultCount: results.length });
636660
return results;
637661
}

0 commit comments

Comments
 (0)