Skip to content

Query-driven sync doesn't work when collections are used in subqueries - loadSubsetOptions #1005

@5h1rU

Description

@5h1rU
  • I've validated the bug against the latest version of DB packages

Describe the bug
When using a collection with syncMode: 'on-demand' in a subquery, the filters from the subquery's .where() clauses are not passed to the collection's queryFn via ctx.meta?.loadSubsetOptions. This prevents query-driven sync from working when collections are used in subqueries, forcing the API to load all data instead of filtered subsets.

To Reproduce
Steps to reproduce the behavior:

  1. Create a collection with syncMode: 'on-demand' and a queryFn that uses parseLoadSubsetOptions:
const ordersCollection = createCollection(
  queryCollectionOptions({
    syncMode: 'on-demand',
    queryKey: ['orders'],
    queryFn: async ctx => {
      const { filters, sorts, limit } = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions);
      
      console.log('Filters:', filters); // Empty array when used in subquery
      console.log('loadSubsetOptions.where:', ctx.meta?.loadSubsetOptions?.where); // undefined
      
      const params = {};
      filters.forEach(({ field, operator, value }) => {
        // Map filters to API params - never executes when in subquery
      });
      
      return api.listOrders(params);
    },
  })
);
  1. Use the collection in a subquery with filters:
const query = q => {
  const prepaidOrder = q
    .from({ prepaidOrder: ordersCollection })
    .where(({ prepaidOrder }) => gte(prepaidOrder.scheduled_at, today))
    .where(({ prepaidOrder }) => eq(prepaidOrder.status, 'queued'))
    .orderBy(({ prepaidOrder }) => prepaidOrder.scheduled_at, 'asc');
    
  return q
    .from({ charge: chargesCollection })
    .fullJoin({ prepaidOrder }, ({ charge, prepaidOrder }) => 
      eq(charge?.address_id, prepaidOrder?.address_id)
    );
};
  1. Observe that ctx.meta?.loadSubsetOptions.where is undefined and filters is an empty array []

Expected behavior
When a collection is used in a subquery with .where() clauses, those filters should be extracted and passed to the collection's queryFn via ctx.meta?.loadSubsetOptions.where, allowing the API to be called with the correct filter parameters. This should work the same way as when the collection is queried directly.

Actual behavior

  • ctx.meta?.loadSubsetOptions.where is undefined
  • ctx.meta?.loadSubsetOptions.orderBy is undefined
  • parseLoadSubsetOptions() returns empty arrays for filters and sorts
  • Only ctx.meta?.loadSubsetOptions.subscription is present (a CollectionSubscription instance)
  • The queryFn cannot determine what filters to apply, so it either loads all data or applies incorrect defaults

Code Example

import { createCollection, parseLoadSubsetOptions } from '@tanstack/react-db';
import { queryCollectionOptions } from '@tanstack/query-db-collection';
import { gte, eq } from '@tanstack/react-db';

// Collection with on-demand sync
const ordersCollection = createCollection(
  queryCollectionOptions({
    syncMode: 'on-demand',
    queryKey: ['orders'],
    getKey: (item) => item.id,
    queryFn: async ctx => {
      const { filters, sorts, limit } = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions);
      
      // ❌ PROBLEM: When used in subquery, these are all empty/undefined
      // filters: []
      // sorts: []
      // ctx.meta?.loadSubsetOptions.where: undefined
      
      const params = {};
      filters.forEach(({ field, operator, value }) => {
        // This never runs when collection is in subquery
        if (operator === 'gte' && field[field.length - 1] === 'scheduled_at') {
          params.scheduled_at_min = value;
        }
        if (operator === 'eq' && field[field.length - 1] === 'status') {
          params.status = value;
        }
      });
      
      return api.listOrders(params); // Called without filters
    },
  })
);

// Direct query - WORKS ✅
const directQuery = q => 
  q
    .from({ order: ordersCollection })
    .where(({ order }) => gte(order.scheduled_at, today))
    .where(({ order }) => eq(order.status, 'queued'));
// ✅ Filters are passed to queryFn correctly

// Subquery - DOESN'T WORK ❌
const subqueryExample = q => {
  const prepaidOrder = q
    .from({ prepaidOrder: ordersCollection })
    .where(({ prepaidOrder }) => gte(prepaidOrder.scheduled_at, today))
    .where(({ prepaidOrder }) => eq(prepaidOrder.status, 'queued'));
    
  return q
    .from({ charge: chargesCollection })
    .fullJoin({ prepaidOrder }, ({ charge, prepaidOrder }) => 
      eq(charge?.address_id, prepaidOrder?.address_id)
    );
};
// ❌ Filters are NOT passed to queryFn

Additional context

  • Version: @tanstack/react-db@0.5.11, @tanstack/query-db-collection@1.0.6
  • Impact: This prevents efficient query-driven sync for collections used in complex queries with joins, forcing full dataset loads and client-side filtering
  • Workaround: Currently loading all data and filtering client-side, which defeats the purpose of on-demand sync for large datasets
  • The subscription object is available in ctx.meta?.loadSubsetOptions.subscription but doesn't expose the where expression that could be used to extract filters

Related:
This is related to query-driven sync feature introduced in v0.5. The documentation shows it working for direct queries, but doesn't mention this limitation with subqueries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions