Skip to content

materialize() subquery resolves to [] when its correlation predicate targets a joined alias #1704

Description

@Jaime02
  • I've validated the bug against the latest version of DB packages (@tanstack/db@0.6.17)

Describe the bug

A materialize() correlated subquery silently resolves to an empty array when its correlation
predicate references a joined alias instead of the subquery's own from alias.

// ❌ always [] — correlation reaches `part.id` through the joined `order` alias
materialize(
  q.from({ production: productions })
   .innerJoin({ order: orders }, ({ production, order }) => eq(production.orderId, order.id))
   .where(({ order }) => eq(order.partId, part.id))
)

// ✅ works — correlation targets the subquery's own `from` alias
materialize(
  q.from({ order: orders }).where(({ order }) => eq(order.partId, part.id))
)

There is no error or warning — the field is just permanently empty, so the failure is silent data
loss. In our app this quietly dropped two whole relations out of a stock ledger, and it went
unnoticed because the surrounding relations populated correctly.

To Reproduce

  1. npm i @tanstack/db@0.6.17
  2. Save as repro.cjs:
const {
  createCollection, createLiveQueryCollection, eq, localOnlyCollectionOptions, materialize,
} = require("@tanstack/db");

const mk = (id, initialData) =>
  createCollection(localOnlyCollectionOptions({ id, getKey: (r) => r.id, initialData }));

const parts       = mk("part",       [{ id: 1 }]);
const orders      = mk("order",      [{ id: 1, partId: 1 }]);
const productions = mk("production", [{ id: 1, orderId: 1 }]);

const live = createLiveQueryCollection({
  startSync: true,
  query: (q) =>
    q.from({ part: parts })
      .select(({ part }) => ({
        ...part,

        // CONTROL — correlation references this subquery's own `from` alias.
        control: materialize(
          q.from({ order: orders }).where(({ order }) => eq(order.partId, part.id)),
        ),

        // BUG — identical correlation, but reached through a joined alias.
        broken: materialize(
          q.from({ production: productions })
            .innerJoin({ order: orders }, ({ production, order }) =>
              eq(production.orderId, order.id))
            .where(({ order }) => eq(order.partId, part.id))
            .select(({ production }) => ({ ...production })),
        ),
      }))
      .findOne(),
});

setTimeout(() => {
  const row = Array.from(live.entries(), ([, v]) => v)[0];
  console.log("control (expect 1):", row?.control?.length ?? 0);
  console.log("broken  (expect 1):", row?.broken?.length ?? 0);
}, 100);
  1. node repro.cjs
control (expect 1): 1
broken  (expect 1): 0

Expected behavior

broken should contain the single production row, the same as running that join + where
outside materialize().

Additional context

  • The join and predicate are correct in isolation. The exact same
    from(production).innerJoin(order).where(order.partId === 1) as a plain, uncorrelated live query
    returns 1 row. Only the correlated materialize() form returns [].
  • Not an indexing artifact. Adding the indexes suggested by the
    "Join requires an index on \"orderId\"" warning (production.orderId, order.partId,
    order.id, with defaultIndexType: BasicIndex) produces the identical broken: 0.
  • Not a sync-timing artifact. All rows are present before the query is built, localOnly
    collections, and the result is stable indefinitely — it never converges.
  • Reproduced first against Electric-backed collections in a real app, then reduced to the
    localOnly case above.
  • Possibly related to the lazy-target/alias resolution that RFC RFC: Stabilizing includes / nested materialization #1658 (PR 2) restructures — the
    correlation target here is resolved through a joined alias — but this failure mode isn't among
    the issues that RFC enumerates.

Environment

  • @tanstack/db@0.6.17 (current latest), Node v24.14.0
  • Plain Node script, no framework adapter or browser involved

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