From 9585042a480653cf19a00df10e6907671cc3826a Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sat, 2 May 2026 17:57:34 -0300 Subject: [PATCH] fix: avoid eager load when using whereHas --- package-lock.json | 4 +- package.json | 2 +- src/models/builders/ModelQueryBuilder.ts | 2 +- src/models/schemas/ModelSchema.ts | 13 +++-- src/types/relations/BelongsToManyOptions.ts | 6 +- src/types/relations/BelongsToOptions.ts | 6 +- src/types/relations/HasManyOptions.ts | 6 +- src/types/relations/HasOneOptions.ts | 6 +- tests/fixtures/models/Product.ts | 4 ++ .../models/builders/ModelQueryBuilderTest.ts | 58 +++++++++++++++++++ 10 files changed, 82 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1064ca8d..b8d31216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/database", - "version": "5.48.0", + "version": "5.49.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/database", - "version": "5.48.0", + "version": "5.49.0", "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1" diff --git a/package.json b/package.json index 8343eb9e..ed71a230 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/database", - "version": "5.48.0", + "version": "5.49.0", "description": "The Athenna database handler for SQL/NoSQL.", "license": "MIT", "author": "João Lenon ", diff --git a/src/models/builders/ModelQueryBuilder.ts b/src/models/builders/ModelQueryBuilder.ts index 722f67ad..bcafe32c 100644 --- a/src/models/builders/ModelQueryBuilder.ts +++ b/src/models/builders/ModelQueryBuilder.ts @@ -598,7 +598,7 @@ export class ModelQueryBuilder< > ) => any ) { - const options = this.schema.includeRelation(relation, closure) + const options = this.schema.includeWhereHasRelation(relation, closure) super.whereExists(query => { switch (options.type) { diff --git a/src/models/schemas/ModelSchema.ts b/src/models/schemas/ModelSchema.ts index 0a02f239..54c21923 100644 --- a/src/models/schemas/ModelSchema.ts +++ b/src/models/schemas/ModelSchema.ts @@ -318,11 +318,13 @@ export class ModelSchema extends Macroable { } /** - * Return the relation options only from relations - * that are included. + * Relation options used only when eager-loading related rows (`with()` / + * {@link ModelSchema.includeRelation includeRelation}). Constraints from + * `whereHas()` are not included here; use `with()` when the response must + * contain related models. */ public getIncludedRelations(): RelationOptions[] { - return this.relations.filter(r => r.isIncluded || r.isWhereHasIncluded) + return this.relations.filter(r => r.isIncluded) } /** @@ -370,8 +372,9 @@ export class ModelSchema extends Macroable { } /** - * Include a relation by setting the isWhereHasIncluded - * option to true. + * Marks relation metadata for a `whereHas()` constraint (stores closure). + * Does not eager-load related rows; only {@link ModelSchema.includeRelation} + * participates in {@link ModelSchema.getIncludedRelations eager loading}. */ public includeWhereHasRelation( property: string | ModelRelations, diff --git a/src/types/relations/BelongsToManyOptions.ts b/src/types/relations/BelongsToManyOptions.ts index d7c62f7f..ea286732 100644 --- a/src/types/relations/BelongsToManyOptions.ts +++ b/src/types/relations/BelongsToManyOptions.ts @@ -68,10 +68,8 @@ export type BelongsToManyOptions< isIncluded?: boolean /** - * Set if the model will be included when fetching - * data. - * If this option is true, you don't need to call - * methods like `whereHas()` to eager load your relation. + * Internal flag when `whereHas()` applies a constraint on this relation. + * Does not eager-load; use {@link isIncluded} / `with()` to load related rows. * * @default false */ diff --git a/src/types/relations/BelongsToOptions.ts b/src/types/relations/BelongsToOptions.ts index 452b99c2..8d897f5b 100644 --- a/src/types/relations/BelongsToOptions.ts +++ b/src/types/relations/BelongsToOptions.ts @@ -59,10 +59,8 @@ export type BelongsToOptions< isIncluded?: boolean /** - * Set if the model will be included when fetching - * data. - * If this option is true, you don't need to call - * methods like `whereHas()` to eager load your relation. + * Internal flag when `whereHas()` applies a constraint on this relation. + * Does not eager-load; use {@link isIncluded} / `with()` to load related rows. * * @default false */ diff --git a/src/types/relations/HasManyOptions.ts b/src/types/relations/HasManyOptions.ts index 0f4ea13f..e26f311f 100644 --- a/src/types/relations/HasManyOptions.ts +++ b/src/types/relations/HasManyOptions.ts @@ -59,10 +59,8 @@ export type HasManyOptions< isIncluded?: boolean /** - * Set if the model will be included when fetching - * data. - * If this option is true, you don't need to call - * methods like `whereHas()` to eager load your relation. + * Internal flag when `whereHas()` applies a constraint on this relation. + * Does not eager-load; use {@link isIncluded} / `with()` to load related rows. * * @default false */ diff --git a/src/types/relations/HasOneOptions.ts b/src/types/relations/HasOneOptions.ts index bfc5e7ee..cc78b6f6 100644 --- a/src/types/relations/HasOneOptions.ts +++ b/src/types/relations/HasOneOptions.ts @@ -59,10 +59,8 @@ export type HasOneOptions< isIncluded?: boolean /** - * Set if the model will be included when fetching - * data. - * If this option is true, you don't need to call - * methods like `whereHas()` to eager load your relation. + * Internal flag when `whereHas()` applies a constraint on this relation. + * Does not eager-load; use {@link isIncluded} / `with()` to load related rows. * * @default false */ diff --git a/tests/fixtures/models/Product.ts b/tests/fixtures/models/Product.ts index 2f702919..9c8eec3f 100644 --- a/tests/fixtures/models/Product.ts +++ b/tests/fixtures/models/Product.ts @@ -17,4 +17,8 @@ export class Product extends BaseModel { @Column() public id: string + + /** FK for User `HasMany` products (defaults to userId). */ + @Column() + public userId: string } diff --git a/tests/unit/models/builders/ModelQueryBuilderTest.ts b/tests/unit/models/builders/ModelQueryBuilderTest.ts index d2cee410..c7e2bfec 100644 --- a/tests/unit/models/builders/ModelQueryBuilderTest.ts +++ b/tests/unit/models/builders/ModelQueryBuilderTest.ts @@ -2307,4 +2307,62 @@ export default class ModelQueryBuilderTest { assert.calledOnceWith(Database.driver.limit, limitValue) } + + @Test() + public async whereHasShouldNotEagerLoadRelations({ assert }: Context) { + let findManyCalls = 0 + + Mock.stub(Database.driver, 'findMany').callsFake(async () => { + findManyCalls++ + + return [{ id: '1', name: 'John Doe' }] + }) + + await User.query() + .whereHas('products', qb => qb.where('id', 'p1')) + .findMany() + + assert.equal(findManyCalls, 1) + } + + @Test() + public async withShouldEagerLoadRelations({ assert }: Context) { + let findManyCalls = 0 + + Mock.stub(Database.driver, 'findMany').callsFake(async () => { + findManyCalls++ + + if (findManyCalls === 1) { + return [{ id: '1', name: 'John Doe' }] + } + + return [{ id: 'p1', userId: '1' }] + }) + + await User.query().with('products').findMany() + + assert.equal(findManyCalls, 2) + } + + @Test() + public async whereHasWithSameRelationShouldStillEagerLoad({ assert }: Context) { + let findManyCalls = 0 + + Mock.stub(Database.driver, 'findMany').callsFake(async () => { + findManyCalls++ + + if (findManyCalls === 1) { + return [{ id: '1', name: 'John Doe' }] + } + + return [{ id: 'p1', userId: '1' }] + }) + + await User.query() + .whereHas('products', qb => qb.where('id', 'p1')) + .with('products') + .findMany() + + assert.equal(findManyCalls, 2) + } }