Skip to content

fix(db): emit change on order-only reorder in ordered live queries#1601

Open
v-anton wants to merge 2 commits into
TanStack:mainfrom
v-anton:fix/orderby-reorder-emission
Open

fix(db): emit change on order-only reorder in ordered live queries#1601
v-anton wants to merge 2 commits into
TanStack:mainfrom
v-anton:fix/orderby-reorder-emission

Conversation

@v-anton

@v-anton v-anton commented Jun 19, 2026

Copy link
Copy Markdown

🎯 Changes

A live query with orderBy does not emit a change when a row's position changes but its projected value does not. For example:

q.from({ s: source })
 .orderBy(({ s }) => s.value)
 .select(({ s }) => ({ id: s.id }))   // sort field not in projection

When a row's value changes so its position moves, the projected value ({ id }) is byte-identical, so the collection's value-diff (deepEquals in state.ts) suppresses the update. The live query's SortedMap re-sorts internally, but subscribeChanges emits nothing — so useLiveQuery keeps rendering the stale order. Any projection that omits the changing orderBy field hits this (it is not specific to id-only).

Fix

The orderBy operator already streams the move as a retract (old value + old orderByIndex) plus an insert (new value + new index). We capture the retracted side in accumulateChanges and, after each flush, emit an update directly only when the index changed and the value is unchanged — the exact case the value-diff swallows. This mirrors the existing includes-materialization direct-emit pattern and leaves generic collection code untouched.

  • Zero cost for collections without orderBy (the gate short-circuits on undefined index).
  • No double-emit: the gate (value unchanged) is mutually exclusive with state.ts's emit (value changed).
  • Only adds emits; never removes or alters existing ones.

✅ Checklist

  • Tested locally with pnpm test.

New tests: packages/db/tests/live-query-orderby-reorder.test.ts (id-only reorder emits; exactly-one-emit when sort field projected; no spurious emit; limit window) and packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx (useLiveQuery renders correct order). Full @tanstack/db (2391) and @tanstack/react-db (95) suites pass.

🚀 Release Impact

  • Affects published code; changeset included (@tanstack/db patch).

Summary by CodeRabbit

Bug Fixes

  • Fixed live queries with orderBy to correctly detect and emit updates when rows are reordered (order-only moves), ensuring UI/order changes are reflected even if projected values don’t change.
  • Prevented duplicate notifications when the ordering field is involved in the projection.

Tests

  • Expanded live-query orderBy reorder test coverage, including projection and limit edge cases.
  • Added coverage for the React useLiveQuery hook to verify reordered results update in the rendered output even when the sort field isn’t selected.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Extends Changes<T> with optional previousValue and previousOrderByIndex fields, then modifies CollectionConfigBuilder to record those fields on retracts, preserve them across key-based re-merges, and call a new emitOrderOnlyMoves method after flushing parent diffs. That method force-emits update events for rows whose projected value is deep-equal but whose orderByIndex changed. Tests and a changeset entry are added.

Changes

orderBy reorder emission for live queries

Layer / File(s) Summary
Metadata extension and accumulation
packages/db/src/query/live/types.ts, packages/db/src/query/live/collection-config-builder.ts
Adds optional previousValue and previousOrderByIndex fields to Changes<T>; imports deepEquals for value comparison; records retract-side metadata during accumulateChanges().
Move detection and forced emission
packages/db/src/query/live/collection-config-builder.ts
Preserves previous metadata across key-based re-merges; invokes emitOrderOnlyMoves() after parent diff flush; implements the method to scan changesToApply for deep-equal-value/different-orderByIndex pairs and force-emit update events via _changes.emitEvents().
Core live query orderBy reorder tests
packages/db/tests/live-query-orderby-reorder.test.ts
Introduces test helpers and suite covering reorder emission with projection, single-event behavior when sort field is projected, silence on non-sort field updates, limit+orderBy window reorders, and edge cases with limit(0) and offset beyond data.
React hook integration and changeset
packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx, .changeset/orderby-reorder-emission.md
Adds useLiveQuery hook test asserting rendered id order updates after a reorder-triggering write; documents the @tanstack/db patch fix in changeset.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • samwillis

🐇 A hop, a skip, the order's changed,
But deep-equal values left me deranged!
Now previousIndex saves the day,
forceEmit tells subscribers: "Hey!"
The rows are sorted — hip hooray! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing live queries to emit changes when rows reorder but their projected values remain unchanged in ordered queries.
Description check ✅ Passed The description fully explains the problem, solution, testing approach, and includes all required checklist items completed. Both the changes and release impact sections are comprehensively detailed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@v-anton

v-anton commented Jun 19, 2026

Copy link
Copy Markdown
Author

Relationship to other open orderBy issues

While investigating I checked the nearby open issues in the ordered-query area. This fix is related but distinct from both:

This PR specifically fixes the case where a top-level ordered live query's projection omits the changing orderBy field, so a reorder is suppressed by the collection's value-diff (state.ts deepEquals) and never reaches subscribers.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/db/tests/live-query-orderby-reorder.test.ts (2)

128-153: ⚡ Quick win

Add explicit limit/offset boundary tests for reorder behavior.

You already cover a standard limit path; please add limit(0) and offset-beyond-length cases to lock down empty-window semantics during reorder updates.

As per coding guidelines, "Handle limit and offset edge cases: consider what happens when limit is 0, undefined, or when offset exceeds data length" and test corner cases in *.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/tests/live-query-orderby-reorder.test.ts` around lines 128 - 153,
The current test covers a standard limit case with limit(3). Add two additional
test cases following the same pattern: one testing the reorder behavior when
limit(0) is used to create an empty result window, and another testing when
offset exceeds the data length to verify empty-window semantics during reorder
updates. Use the same manualCollection, createLiveQueryCollection, write, and
subscribeChanges pattern to verify that reorder updates are handled correctly
when the query window is empty or beyond available data boundaries.

Source: Coding guidelines


9-9: ⚡ Quick win

Remove any from test helper/projection paths and use explicit test-local types.

Typed helper contracts and projected row types keep regressions catchable at compile-time and make assertions safer.

As per coding guidelines, "Avoid using any types; use unknown instead when the type is truly unknown, and provide proper type annotations for return values".

Also applies to: 54-55, 87-87, 95-95, 124-124, 140-140, 152-152

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/tests/live-query-orderby-reorder.test.ts` at line 9, Replace all
instances of the `any` type throughout the file (found at the variable
declaration `let fns: any` and at the other locations noted in lines 54-55, 87,
95, 124, 140, 152) with explicit type annotations. For test helper functions and
projection row types, define appropriate test-local types that clearly express
what these variables and functions should return, rather than using `any` or
`unknown`. This ensures better type safety and makes assertions more robust by
allowing the compiler to catch type-related regressions.

Source: Coding guidelines

packages/db/src/query/live/collection-config-builder.ts (1)

1024-1030: 🏗️ Heavy lift

Avoid reaching into _changes via any; delegate through a typed internal method.

This hard-couples CollectionConfigBuilder to Collection internals and weakens compile-time safety. Prefer an internal delegate on Collection/sync methods for forced emits, then call that API here.

♻️ Proposed direction
- const changesManager = (config.collection as any)._changes as {
-   emitEvents: (
-     changes: Array<ChangeMessage<TResult>>,
-     forceEmit?: boolean,
-   ) => void
- }
- changesManager.emitEvents(moves, true)
+ config.collection.emitInternalChanges(moves, { forceEmit: true })

As per coding guidelines, "Encapsulate implementation details within responsible classes; use delegation to maintain clean boundaries between components" and "Avoid exposing internal properties directly; instead add public methods that delegate to internal implementations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/query/live/collection-config-builder.ts` around lines 1024 -
1030, The code directly accesses the private _changes property on the collection
object through any type casting to call emitEvents, which breaks encapsulation
and type safety. Instead of reaching into _changes via any, create an internal
delegate method on the Collection class (such as emitChangesForced or similar)
that encapsulates the _changes.emitEvents call, then replace the direct
changesManager assignment and emitEvents invocation in the current code with a
call to this new internal method on the collection object. This maintains clean
boundaries between components while preserving the forced emit behavior.

Source: Coding guidelines

packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx (2)

37-37: ⚡ Quick win

Replace any in result mapping with a concrete row type.

This keeps hook assertions type-safe and avoids masking contract drift in useLiveQuery data shape.

As per coding guidelines, "Avoid using any types; use unknown instead when the type is truly unknown, and provide proper type annotations for return values".

Also applies to: 44-44

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx` at line 37,
The map function in the result.current.data transformation uses `any` type
annotation for the row parameter, which reduces type safety. Identify the
concrete row type that useLiveQuery returns (likely defined in your test setup
or data models), and replace the `any` type annotation with that concrete type
in both the mapping operation at line 37 and the additional occurrence at line
44. This ensures type-safe assertions and prevents masking any contract drift in
the useLiveQuery data shape.

Source: Coding guidelines


21-47: ⚡ Quick win

Add a rapid consecutive updates test to cover hook-level async race ordering.

A case with back-to-back writes that reorder in opposite directions would strengthen confidence that stale order does not render between emissions.

As per coding guidelines, test corner cases including "async race conditions where operations may resolve in unexpected order".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx` around lines
21 - 47, Add a new test case within the existing describe block for useLiveQuery
orderBy + select(id-only) that tests rapid consecutive updates with reordering
in opposite directions. This test should perform multiple back-to-back writes
using act() that cause the items to reorder in alternating directions (for
example, one update that moves an item to the top, followed immediately by
another that moves it to the bottom), and then verify using waitFor() that the
final rendered order matches the expected result without any stale intermediate
orders appearing. This will ensure the hook properly handles async race
conditions where multiple reordering operations occur in quick succession.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/tests/live-query-orderby-reorder.test.ts`:
- Line 4: The `flush` function uses a hardcoded setTimeout with a fixed 20ms
delay, making tests timing-sensitive under CI load. Instead of relying on this
sleep-based approach, replace the `flush` function implementation to use
condition-based waits by leveraging testing utilities like `waitFor` to
explicitly wait on observable state changes. Identify the specific order or
event expectations that each test needs to verify after calling `flush`, and
replace the fixed sleep with explicit waits on those observable state conditions
to ensure reliable test execution regardless of system load.

---

Nitpick comments:
In `@packages/db/src/query/live/collection-config-builder.ts`:
- Around line 1024-1030: The code directly accesses the private _changes
property on the collection object through any type casting to call emitEvents,
which breaks encapsulation and type safety. Instead of reaching into _changes
via any, create an internal delegate method on the Collection class (such as
emitChangesForced or similar) that encapsulates the _changes.emitEvents call,
then replace the direct changesManager assignment and emitEvents invocation in
the current code with a call to this new internal method on the collection
object. This maintains clean boundaries between components while preserving the
forced emit behavior.

In `@packages/db/tests/live-query-orderby-reorder.test.ts`:
- Around line 128-153: The current test covers a standard limit case with
limit(3). Add two additional test cases following the same pattern: one testing
the reorder behavior when limit(0) is used to create an empty result window, and
another testing when offset exceeds the data length to verify empty-window
semantics during reorder updates. Use the same manualCollection,
createLiveQueryCollection, write, and subscribeChanges pattern to verify that
reorder updates are handled correctly when the query window is empty or beyond
available data boundaries.
- Line 9: Replace all instances of the `any` type throughout the file (found at
the variable declaration `let fns: any` and at the other locations noted in
lines 54-55, 87, 95, 124, 140, 152) with explicit type annotations. For test
helper functions and projection row types, define appropriate test-local types
that clearly express what these variables and functions should return, rather
than using `any` or `unknown`. This ensures better type safety and makes
assertions more robust by allowing the compiler to catch type-related
regressions.

In `@packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx`:
- Line 37: The map function in the result.current.data transformation uses `any`
type annotation for the row parameter, which reduces type safety. Identify the
concrete row type that useLiveQuery returns (likely defined in your test setup
or data models), and replace the `any` type annotation with that concrete type
in both the mapping operation at line 37 and the additional occurrence at line
44. This ensures type-safe assertions and prevents masking any contract drift in
the useLiveQuery data shape.
- Around line 21-47: Add a new test case within the existing describe block for
useLiveQuery orderBy + select(id-only) that tests rapid consecutive updates with
reordering in opposite directions. This test should perform multiple
back-to-back writes using act() that cause the items to reorder in alternating
directions (for example, one update that moves an item to the top, followed
immediately by another that moves it to the bottom), and then verify using
waitFor() that the final rendered order matches the expected result without any
stale intermediate orders appearing. This will ensure the hook properly handles
async race conditions where multiple reordering operations occur in quick
succession.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45978f9c-2f09-4ecd-b14c-7c28e43ad837

📥 Commits

Reviewing files that changed from the base of the PR and between 9a5e8e5 and 66bf854.

📒 Files selected for processing (5)
  • .changeset/orderby-reorder-emission.md
  • packages/db/src/query/live/collection-config-builder.ts
  • packages/db/src/query/live/types.ts
  • packages/db/tests/live-query-orderby-reorder.test.ts
  • packages/react-db/tests/useLiveQuery-orderby-reorder.test.tsx

Comment thread packages/db/tests/live-query-orderby-reorder.test.ts Outdated
…ary cases

- Drop fixed setTimeout flushes (live query is synchronous on commit)
- Type the sync helper and projections instead of any
- Add limit(0) and offset-beyond-length empty-window reorder cases
@v-anton

v-anton commented Jun 19, 2026

Copy link
Copy Markdown
Author

Thanks @coderabbitai — addressed the test feedback in 18f158d5:

  • Flaky fixed setTimeout flushes → removed. The live query processes synchronously on commit, so the tests now assert directly after each write (no sleeps, deterministic).
  • limit/offset boundary cases → added limit(0) and offset-beyond-length tests asserting the empty window stays empty and silent across a reorder.
  • any in the test → typed the sync helper (SyncFns via SyncConfig) and projections (idsOf).

On the (config.collection as any)._changes.emitEvents(...) nitpick: I've left it as-is intentionally. It mirrors the existing includes-materialization path in the same file (collection-config-builder.ts, the inlineReEmitKeys block), which uses the identical (collection as any)._changes pattern to force-emit. Introducing a new public Collection.emitInternalChanges(...) delegate to satisfy this one call would add public API surface and leave that established precedent inconsistent — better handled as a separate, focused refactor of both sites if the maintainers want it. Happy to do that follow-up on request.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@v-anton: Thanks for the thorough follow-up — the test improvements look solid:

  • Deterministic assertions after synchronous commits is strictly better than timer-based flushing.
  • limit(0) and offset-beyond-length boundary cases are exactly the kind of edge coverage that prevents regressions.
  • Typed projections and sync helper removes the loose any from test code where it was avoidable.

On the (config.collection as any)._changes.emitEvents(...) point — your rationale is well-reasoned. Keeping it consistent with the existing inlineReEmitKeys precedent in the same file is the right call; a targeted refactor of both sites together makes more sense than asymmetrically introducing a new public delegate just for this one call. Happy to track that as a follow-up if you'd like a separate issue opened for it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/db/tests/live-query-orderby-reorder.test.ts (1)

49-203: ⚡ Quick win

Add missing corner-case tests required by test guidelines (single-element + async race).

This suite now covers the main repro and limit/offset edges, but it still misses guideline-required corner coverage for a single-element collection and an async race/ordering scenario (e.g., back-to-back updates that reorder in opposite directions and assert final order + emitted events are not stale).

As per coding guidelines, "**/*.test.{ts,tsx,js}: Test corner cases including: ... single-element collections ... async race conditions ... and limit/offset edge cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/tests/live-query-orderby-reorder.test.ts` around lines 49 - 203,
Add two new test cases to the test suite that cover the required corner cases.
First, add a single-element collection test using the same pattern as the
existing tests (manualCollection with one item, createLiveQueryCollection with
orderBy and select, write an update that triggers a reorder, and verify the
collection remains correct and emits appropriate events). Second, add an async
race condition test that writes two back-to-back updates causing opposite
reorders (for example, first update moves an item to the front, then immediately
updates another item to push the first item back), and verify that the final
order is correct and that the emitted events reflect the actual state
transitions without stale or duplicate events.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/db/tests/live-query-orderby-reorder.test.ts`:
- Around line 49-203: Add two new test cases to the test suite that cover the
required corner cases. First, add a single-element collection test using the
same pattern as the existing tests (manualCollection with one item,
createLiveQueryCollection with orderBy and select, write an update that triggers
a reorder, and verify the collection remains correct and emits appropriate
events). Second, add an async race condition test that writes two back-to-back
updates causing opposite reorders (for example, first update moves an item to
the front, then immediately updates another item to push the first item back),
and verify that the final order is correct and that the emitted events reflect
the actual state transitions without stale or duplicate events.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcc9819d-c473-496c-89f0-93da1040c68e

📥 Commits

Reviewing files that changed from the base of the PR and between 66bf854 and 18f158d.

📒 Files selected for processing (1)
  • packages/db/tests/live-query-orderby-reorder.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant