From 063e1caacf99f0c3191b58a6ed4aa7d1306daed0 Mon Sep 17 00:00:00 2001 From: rajat12826 Date: Sun, 30 Aug 2026 13:19:39 +0530 Subject: [PATCH 1/4] fix(mongo-orm): retain relation fields in $project when select and include are combined When chaining .select() and .include(), the compiled $project stage only projected selectedFields, dropping relation aliases produced by $lookup. Add included relation names to the projection so joined data is preserved in the returned document. Signed-off-by: rajat12826 --- .../5-query-builders/orm/src/compile.ts | 3 + .../5-query-builders/orm/test/compile.test.ts | 68 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/packages/2-mongo-family/5-query-builders/orm/src/compile.ts b/packages/2-mongo-family/5-query-builders/orm/src/compile.ts index d6c0eb9979e0..c08f34548ef5 100644 --- a/packages/2-mongo-family/5-query-builders/orm/src/compile.ts +++ b/packages/2-mongo-family/5-query-builders/orm/src/compile.ts @@ -73,6 +73,9 @@ export function compileMongoQuery( for (const field of state.selectedFields) { projection[field] = 1; } + for (const inc of state.includes) { + projection[inc.relationName] = 1; + } if (!Object.hasOwn(projection, '_id')) { projection['_id'] = 0; } diff --git a/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts index 536e68773f8a..4c625606b16b 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts @@ -300,4 +300,72 @@ describe('compileMongoQuery', () => { expect(plan.resultShape.fields['author']?.kind).toBe('unknown'); }); }); + + describe('select combined with include', () => { + it('$project retains to-many relation alias from includes', () => { + const state: MongoCollectionState = { + ...emptyCollectionState(), + selectedFields: ['name'], + includes: [ + { + relationName: 'posts', + from: 'posts', + localField: '_id', + foreignField: 'authorId', + cardinality: '1:N', + }, + ], + }; + const plan = compileMongoQuery('users', state, testHash, testUserModel); + const projectStage = stages(plan).find((s) => s.kind === 'project') as + | MongoProjectStage + | undefined; + expect(projectStage).toBeDefined(); + expect(projectStage!.projection).toEqual({ name: 1, posts: 1, _id: 0 }); + }); + + it('$project retains to-one relation alias from includes', () => { + const state: MongoCollectionState = { + ...emptyCollectionState(), + selectedFields: ['title'], + includes: [ + { + relationName: 'author', + from: 'users', + localField: 'authorId', + foreignField: '_id', + cardinality: 'N:1', + }, + ], + }; + const plan = compileMongoQuery('posts', state, testHash, testPostModel); + const projectStage = stages(plan).find((s) => s.kind === 'project') as + | MongoProjectStage + | undefined; + expect(projectStage).toBeDefined(); + expect(projectStage!.projection).toEqual({ title: 1, author: 1, _id: 0 }); + }); + + it('$project retains _id when explicitly selected alongside includes', () => { + const state: MongoCollectionState = { + ...emptyCollectionState(), + selectedFields: ['_id', 'name'], + includes: [ + { + relationName: 'posts', + from: 'posts', + localField: '_id', + foreignField: 'authorId', + cardinality: '1:N', + }, + ], + }; + const plan = compileMongoQuery('users', state, testHash, testUserModel); + const projectStage = stages(plan).find((s) => s.kind === 'project') as + | MongoProjectStage + | undefined; + expect(projectStage).toBeDefined(); + expect(projectStage!.projection).toEqual({ _id: 1, name: 1, posts: 1 }); + }); + }); }); From 66327728e1a6c7a8cbf275a3f252078138394d33 Mon Sep 17 00:00:00 2001 From: willbot Date: Thu, 24 Sep 2026 20:12:52 +0200 Subject: [PATCH 2/4] test(mongo-orm): run select() with include() against MongoDB The compile tests only check the $project object. These tests run the query against mongodb-memory-server and assert the whole returned row, for a 1:N include (users.tasks) and an N:1 include (tasks.assignee). Neither query selects its join key, which shows $lookup still sees it because $project runs last. Both tests fail with the compile.ts change reverted: the relation comes back undefined. Included documents are not codec-decoded yet, so their ids are matched with expect.anything() rather than pinned to ObjectId. The FL-08 block now covers N:1 as well, so its name drops "1:N". Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../test/integration/orm-ergonomics.test.ts | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts index f6e186ce1a35..b8b46ec4d758 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts @@ -227,7 +227,7 @@ describe('ORM ergonomics integration (FL-04, FL-06, FL-08)', { }); }); - describe('FL-08: 1:N reference relation include', () => { + describe('FL-08: reference relation include', () => { it('include() on 1:N relation returns array of related documents', async () => { const orm = mongoOrm({ contract, executor: runtime }); const user = await orm.users.create(defaultUserData); @@ -266,5 +266,52 @@ describe('ORM ergonomics integration (FL-04, FL-06, FL-08)', { const tasks = (result as Record)['tasks'] as unknown[]; expect(tasks).toEqual([]); }); + + it('select() keeps a 1:N include whose join key is not selected', async () => { + const orm = mongoOrm({ contract, executor: runtime }); + const user = await orm.users.create(defaultUserData); + await orm.tasks.create({ + title: 'Task 1', + type: 'bug', + assigneeId: user._id as string, + } as never); + + const result = await orm.users + .select('name') + .include('tasks') + .where({ _id: user._id as string }) + .first(); + + expect(result).toEqual({ + name: 'Alice', + tasks: [ + { _id: expect.anything(), title: 'Task 1', type: 'bug', assigneeId: expect.anything() }, + ], + }); + }); + + it('select() keeps an N:1 include whose join key is not selected', async () => { + const orm = mongoOrm({ contract, executor: runtime }); + const user = await orm.users.create(defaultUserData); + await orm.tasks.create({ + title: 'Task 1', + type: 'bug', + assigneeId: user._id as string, + } as never); + + const result = await orm.tasks.select('title').include('assignee').first(); + + expect(result).toEqual({ + title: 'Task 1', + assignee: { + _id: expect.anything(), + name: 'Alice', + email: 'alice@test.com', + loginCount: 0, + tags: [], + homeAddress: null, + }, + }); + }); }); }); From ec9e2b454c2a69a10fefeadbea2fe30d2c4703d1 Mon Sep 17 00:00:00 2001 From: willbot Date: Thu, 24 Sep 2026 20:13:46 +0200 Subject: [PATCH 3/4] test(mongo-orm): pin stage order for a to-one include with select() The to-one compile test now asserts the stages are lookup, unwind, project. The only other stage-order test uses a 1:N include, which has no $unwind, so nothing checked that $project runs after it. With $project moved first, the projection object is unchanged and the old assertion still passed; the new one fails. Drop the test for an explicit _id alongside includes. This change does not touch the _id rule, "preserves _id when explicitly selected" already covers it, and the to-many test already shows the relation is kept. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../5-query-builders/orm/test/compile.test.ts | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts index 4c625606b16b..71c6b2848241 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/compile.test.ts @@ -339,33 +339,12 @@ describe('compileMongoQuery', () => { ], }; const plan = compileMongoQuery('posts', state, testHash, testPostModel); + expect(stages(plan).map((s) => s.kind)).toEqual(['lookup', 'unwind', 'project']); const projectStage = stages(plan).find((s) => s.kind === 'project') as | MongoProjectStage | undefined; expect(projectStage).toBeDefined(); expect(projectStage!.projection).toEqual({ title: 1, author: 1, _id: 0 }); }); - - it('$project retains _id when explicitly selected alongside includes', () => { - const state: MongoCollectionState = { - ...emptyCollectionState(), - selectedFields: ['_id', 'name'], - includes: [ - { - relationName: 'posts', - from: 'posts', - localField: '_id', - foreignField: 'authorId', - cardinality: '1:N', - }, - ], - }; - const plan = compileMongoQuery('users', state, testHash, testUserModel); - const projectStage = stages(plan).find((s) => s.kind === 'project') as - | MongoProjectStage - | undefined; - expect(projectStage).toBeDefined(); - expect(projectStage!.projection).toEqual({ _id: 1, name: 1, posts: 1 }); - }); }); }); From c9104dc734c48096416db10b784b0259c81b6b5c Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 25 Sep 2026 07:24:34 +0200 Subject: [PATCH 4/4] test(mongo-orm): leave the include block name to the label cleanup #30410 renames the test blocks in this file, so this PR keeps the block name as it is on main and only adds tests inside it. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../orm/test/integration/orm-ergonomics.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts index b8b46ec4d758..01fe4a792dd7 100644 --- a/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts +++ b/packages/2-mongo-family/5-query-builders/orm/test/integration/orm-ergonomics.test.ts @@ -227,7 +227,7 @@ describe('ORM ergonomics integration (FL-04, FL-06, FL-08)', { }); }); - describe('FL-08: reference relation include', () => { + describe('FL-08: 1:N reference relation include', () => { it('include() on 1:N relation returns array of related documents', async () => { const orm = mongoOrm({ contract, executor: runtime }); const user = await orm.users.create(defaultUserData);