Repository navigation
Description
Summary
A <HasMany> with filter / orderBy / limit / offset fetches its filtered subquery and, next to it, the plain unfiltered relation with the same nested fields. Every filtered or paginated has-many therefore also loads the whole relation.
Environment
@contember/bindx@0.1.54(version installed in the reporting project)- Reproduces on
contember/bindx@mainas of6f02df1(v0.1.59) - Failing test:
tests/react/relations/hasMany/filteredHasManyFetchesUnfilteredRelation.test.tsx - Branch:
bug/hasmany-filter-also-fetches-unfiltered-relation
Reproduction
<Entity entity={schema.Article} by={{ id: 'article-1' }}>
{article => (
<HasMany field={article.tags} filter={{ name: { eq: 'React' } }}>
{tag => <Field field={tag.name} />}
</HasMany>
)}
</Entity>The get query spec for Article contains two tags fields.
Expected behavior
One field: the aliased subquery with the filter, tags_<hash>(filter: { name: { eq: 'React' } }) { id name }.
Actual behavior
{ name: 'tags', filter: undefined } // plain, unfiltered tags { id name }
{ name: 'tags_g6fozb', filter: { name: { eq: 'React' } } }
On a real relation this loads every row of the relation that the UI only shows filtered or one page of; on a large has-many (comments, enrollments, …) it is the expensive half of the query.
Suspected root cause
HasMany.getSelection (packages/bindx-react/src/jsx/components/HasMany.tsx, around line 77) calls props.field.map(...) on the field it receives during collection, to get a collector entity for the children:
fullField.map((item, index) => {
const scopeRef = (item as unknown as Record<symbol, unknown>)[SCOPE_REF]
...
result.syntheticChildren = props.children(item, index)
return null
})That field is the outer collector proxy, so map registers the relation under its plain name in the outer SelectionScope and the children's reads land in that scope too. getSelection then also returns the aliased field with hasManyParams, and the analyzer merges both into the root selection.
Suggested fix
Collect the children against a detached scope instead of the outer collector: create a fresh SelectionScope (and collector entity for the target entity) inside getSelection, run props.children against it, and return only the aliased field. Alternatively, have the collector's map take the has-many params so it registers the aliased field instead of the plain one. Deferring to maintainers on which fits the collector design.
Found while working on #156.
Activity
vparys commentedon Nov 3, 2026
Digging into the fix showed this is worse than an over-fetch: the plain relation is also what
<HasMany>renders.HasManyImplreadsfieldas given, andEntityHandle.resolveSelectionField('tags')prefers the direct plaintagsover the params alias, so a filtered list shows the whole relation (filter={{ name: { eq: 'React' } }}renders "JavaScriptReact"). The aliased subquery is fetched and never read. Also item 2 ofdocs/issues/036.