From 0a59c236ea413b4b014b881f5076491d3d5d15a8 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Thu, 11 Jun 2026 07:02:35 +0500 Subject: [PATCH] fix(driver-memory): accept the engine QueryAST in aggregate() (analytics fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../driver-memory/src/memory-driver.test.ts | 41 +++++++++++++++++++ .../driver-memory/src/memory-driver.ts | 30 ++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/plugins/driver-memory/src/memory-driver.test.ts b/packages/plugins/driver-memory/src/memory-driver.test.ts index 49ec80763f..6c0397c90c 100644 --- a/packages/plugins/driver-memory/src/memory-driver.test.ts +++ b/packages/plugins/driver-memory/src/memory-driver.test.ts @@ -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); + }); + }); + }); diff --git a/packages/plugins/driver-memory/src/memory-driver.ts b/packages/plugins/driver-memory/src/memory-driver.ts index e688b4c92b..91653451e7 100644 --- a/packages/plugins/driver-memory/src/memory-driver.ts +++ b/packages/plugins/driver-memory/src/memory-driver.ts @@ -625,13 +625,37 @@ export class InMemoryDriver implements IDataDriver { * { $group: { _id: null, avgPrice: { $avg: '$price' } } } * ]); */ - async aggregate(object: string, pipeline: Record[], options?: DriverOptions): Promise { + async aggregate(object: string, pipeline: Record[] | QueryAST, options?: DriverOptions): Promise { + // 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[]; + } + } + 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; }