diff --git a/lib/queryBuilder/operations/FirstOperation.js b/lib/queryBuilder/operations/FirstOperation.js index 543b5951d..404661863 100644 --- a/lib/queryBuilder/operations/FirstOperation.js +++ b/lib/queryBuilder/operations/FirstOperation.js @@ -1,4 +1,5 @@ import { QueryBuilderOperation } from './QueryBuilderOperation.js'; +import { LimitPerParentOperation } from './LimitPerParentOperation.js'; export class FirstOperation extends QueryBuilderOperation { shouldLimit(builder) { @@ -13,8 +14,9 @@ export class FirstOperation extends QueryBuilderOperation { return knexBuilder; } - onAfter3(_, result) { - if (Array.isArray(result)) { + onAfter3(builder, result) { + // Limited per parent, the query still returns the models of all parents: + if (Array.isArray(result) && !builder.has(LimitPerParentOperation)) { return result[0]; } else { return result; diff --git a/lib/queryBuilder/operations/LimitPerParentOperation.js b/lib/queryBuilder/operations/LimitPerParentOperation.js index eef71a8fc..a4c9a559e 100644 --- a/lib/queryBuilder/operations/LimitPerParentOperation.js +++ b/lib/queryBuilder/operations/LimitPerParentOperation.js @@ -1,5 +1,6 @@ import { QueryBuilderOperation } from './QueryBuilderOperation.js'; import { RangeOperation } from './RangeOperation.js'; +import { FirstOperation } from './FirstOperation.js'; import { isNumber } from '../../utils/objectUtils.js'; import { withoutSchema } from './select/Selection.js'; import { getWindowOrder, rowNumberColumn } from './windowOrder.js'; @@ -22,9 +23,11 @@ export class LimitPerParentOperation extends QueryBuilderOperation { // `page()` and `range()` keep limiting all parents, as their total does. if (!builder.has(RangeOperation)) { const { limit = null, offset = null } = knexBuilder._single; + // `first()` only limits the query with `useLimitInFirst`: + const isFirst = builder.has(FirstOperation); - if (limit !== null || offset !== null) { - return { limit, offset: offset ?? 0 }; + if (limit !== null || offset !== null || isFirst) { + return { limit: isFirst ? 1 : limit, offset: offset ?? 0 }; } } diff --git a/lib/queryBuilder/operations/eager/WhereInEagerOperation.js b/lib/queryBuilder/operations/eager/WhereInEagerOperation.js index c1051b054..a923fc058 100644 --- a/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +++ b/lib/queryBuilder/operations/eager/WhereInEagerOperation.js @@ -2,7 +2,6 @@ import * as promiseUtils from '../../../utils/promiseUtils/index.js'; import { EagerOperation } from './EagerOperation.js'; import { LimitPerParentOperation } from '../LimitPerParentOperation.js'; -import { FirstOperation } from '../FirstOperation.js'; import { isMsSql, isOracle, isSqlite } from '../../../utils/knexUtils.js'; import { isObject, asArray, flatten, chunk } from '../../../utils/objectUtils.js'; import { ValidationErrorType } from '../../../model/ValidationError.js'; @@ -133,12 +132,6 @@ export class WhereInEagerOperation extends EagerOperation { } if (this.graphOptions.limitPerParent !== false) { - // `first()` limits the related models of each owner to one, and the query - // still returns the batch of all owners: - if (queryBuilder.has(FirstOperation)) { - queryBuilder.clear(FirstOperation).limit(1); - } - queryBuilder.addOperation( new LimitPerParentOperation('limitPerParent', { findOperation }), [], diff --git a/tests/integration/withGraph.js b/tests/integration/withGraph.js index a240bf614..7cb848bd9 100644 --- a/tests/integration/withGraph.js +++ b/tests/integration/withGraph.js @@ -1201,6 +1201,45 @@ export default (session) => { ]); }); + it('should apply first() of a relation mapping modify per parent', async () => { + class Model1Latest extends Model1 { + static get relationMappings() { + return { + ...super.relationMappings, + latestModel2: { + relation: Model1.HasOneRelation, + modelClass: Model2, + join: { from: 'Model1.id', to: 'model2.model1_id' }, + modify: (query) => query.orderBy('id_col', 'desc').first(), + }, + }; + } + } + + const models = await Model1Latest.query() + .withGraphFetched('latestModel2') + .whereExists(Model1Latest.relatedQuery('model1Relation2')) + .orderBy('id'); + + expect(relatedIds(models, 'latestModel2')).toEqual([ + [1, 2], + [4, 4], + [6, 3], + ]); + }); + + it('should apply first() of an onBuild() hook per parent', async () => { + const models = await fetchModel1s('model1Relation2', (query) => + query.orderBy('id_col', 'desc').onBuild((query) => query.first()), + ); + + expect(relatedIds(models, 'model1Relation2')).toEqual([ + [1, [2]], + [4, [4]], + [6, [3]], + ]); + }); + it('should support limitPerParent: false in defaultGraphOptions', async () => { class Model1Defaults extends Model1 { static get defaultGraphOptions() {