Relation filters (semi/anti-joins) force a full-source load on orderBy+limit queries — forward them to loadSubset instead
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 35/100
- Type d'issue
- Fonctionnalité
- Clarté
- Plutôt claire
- Activité
- Active
- Stack technique
- typescript
- Domaine
- backend-api-design, databases, performance
Piste de recherche
Start in query/compiler/order-by.ts at processOrderBy, then trace OrderedSourceLoader, LoadSubsetOptions, loadSubset forwarding, and getLoadSubsetDemandKey. Run the prototype tests against fake on-demand backends, including honored and ignored relation hints and live child insert/delete cases. Done means limited relation-filtered queries avoid full-source loads while preserving exact pages and live-window updates.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Problem
A windowed live query (orderBy + limit) that filters its root rows by the existence of related rows loads the entire root collection.
Example: "seeks, newest first, 20 per page, that have at least one run by a user outside a hidden set". Expressed as a leftJoin of the child collection (or a distinct child subquery) plus where(not(isUndefined(j.key))) (semi-join), or isUndefined(j.key) (anti-join).
In query/compiler/order-by.ts (processOrderBy), requiresFullSource is set whenever any where references an alias other than the orderBy alias. OrderedSourceLoader then calls loadFullSource(), so the root's loadSubset receives an unlimited request with no limit/cursor. On-demand collections pull the whole table. Adapters that refuse unbounded loads return nothing.
Why it is there (as far as I can tell)
With a limit, the joined side loads lazily by key (SubsetDemandController). Root rows filtered out while their children are still in flight make dataNeeded positive, so loadMore keeps paging. With a slow child side this walks the whole table anyway, so forcing a full load is the safe fallback.
Proposal
- Structured, backend-agnostic hint. Add
LoadSubsetOptions.relations?: Array<{ quantifier: 'some' | 'none'; collectionId: string; on: { parent: string[]; child: string[] }; where?: BasicExpression<boolean> }>.every(p)isnone(not p). A collection may apply it (return only root rows that pass) or ignore it (return a superset), but must never drop a passing row. Local evaluation stays authoritative, so correctness never depends on the collection. - Compiler. In
processOrderBy, split theandconjuncts. Treatnot(isUndefined(j.k))/isUndefined(j.k)as a semi/anti-join whenjis a LEFT join oneq(root.x, j.k)andjis a collection or a simple single-collection subquery. Those conjuncts no longer setrequiresFullSource; they populateinfo.relations, which the root subscription forwards on everyloadSubset(includingrequestLimitedSnapshot/ tie loads). - Loader.
loadMorewaits while any lazy join demand is unsettled or a joined subscription isloadingSubset.settleDemandalready reruns the graph and the loaders. - Joined-side changes. Changes on the joined side must also run the ordered loaders. Removing a child can shrink the window, and today that path (
scheduleGraphRun(undefined)) passes no loader callback, so the window stays short. - Dedupe.
getLoadSubsetDemandKeyincludesrelations, so a relation-narrowed load never dedupes (covers) the same load without the relation.
Backends can honor the hint exactly where they support it. For example, PostgREST: select=*,children!inner(userId)&children.userId=not.in.(…) for some, and a filtered embed with children=is.null for none. An SQL backend can use EXISTS (…).
Prototype
I implemented this as a patch on 0.9.2: about 150 lines of logic per build format, esm and cjs, plus types. It is covered by tests against fake on-demand backends, 40 root rows:
| Case | Today | Hint honored | Hint ignored |
|---|---|---|---|
some, take 5 |
1 unlimited request, all 40 rows | 2 requests, 6 rows | 6 requests, 13 rows fetched, exact page |
some, take 10 |
all 40 rows | 2 requests, 11 rows | 10 requests, 26 rows fetched, exact page |
none / every, take 5 |
all 40 rows | 2 requests, 6 rows | 10 requests, 16 rows fetched, exact page |
(The honored case's second request is the boundary-tie load.)
Tests cover:
- collections that honor the hint (exact pages, no full-source load);
- collections that ignore it (exact pages, stops at the minimal prefix, never loads the full source);
- eager local-only collections;
- live child insert and delete moving a parent into or out of the window, for
some/none/every.
Without the demand gate in step 3, a slow child side (40 ms vs 0 ms) makes the ignoring case load 40 of 40 rows. Happy to open a PR from it if the direction looks right.
Out of scope / open questions
- Should
relationsalso flow for unlimited queries? The prototype only forwards them on limited windows. - Should demand-gating be scoped per join instead of query-wide? Query-wide is simple, but an unrelated include's pending demand delays paging (latency only).
- Relation predicates under
or/not, and joins through subqueries that themselves join, still force a full source in the prototype. - Adapters that page by
offsetcount local rows, which other queries may have inflated. Relation-narrowed loads probably need to page by cursor.
Related: #1657 (loadSubset / pagination core RFC), #1887 (join key segments for remote sources).
- Langage dominant
- TypeScript
- Étoiles
- 4k
- Forks
- 277
- Merge moyen
- 17 h 33 min
- PR mergées (30 j)
- 227
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de TanStack/db
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
Les mainteneurs répondent en général sous 1 jour
-
Electric: scoped move-in snapshots can prematurely resolve awaitTxId and remove optimistic rowsOuverte
Difficulté 4/5 3-5 jours Accessibilité débutants 38/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 1-2 jours Accessibilité débutants 48/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 4/5 1-2 jours Accessibilité débutants 48/100
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de TanStack/db
Issues similaires
-
bug require-triage
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
expressjs/expressjs.com#2549 · 1 réaction ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 83/100
umami-software/umami#4686 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
[Bug]: Device screenshot fails with 500 on iPhone Duo (Xcode 27.2): serve-sim calls simctl without --displayPeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 66/100
pingdotgg/t3code#24900 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
inu-appcenter/UNIDorm-web#411 ·
Les mainteneurs répondent en général sous 1 jour
-
🐛 bug
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
Les mainteneurs répondent en général sous 2 jours