Skip to content

fix(db): support Temporal values in query comparison operators#1519

Open
obeattie wants to merge 9 commits into
TanStack:mainfrom
obeattie:temporal-comparators
Open

fix(db): support Temporal values in query comparison operators#1519
obeattie wants to merge 9 commits into
TanStack:mainfrom
obeattie:temporal-comparators

Conversation

@obeattie

@obeattie obeattie commented May 7, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Fixes a TypeError when query filters use comparison operators (gt/gte/lt/lte) on Temporal values. The evaluators applied JS's native >/< directly, which throws on Temporal types because their valueOf() is designed to throw — a guard against silent miscomparison.

Adds a compareValues(a, b) helper that dispatches to the Temporal types' static .compare() when both operands share a type. Mixed Temporal types and types with no defined ordering (PlainMonthDay) throw a descriptive TypeError rather than fall back to string-lex comparison. Non-Temporal values continue to use native operators.

eq is unchanged — it still goes through normalizeValue, so ZonedDateTime equality treats the zone as part of identity while ordering compares by instant only, mirroring .equals() vs .compare() in the spec.

Tests cover all eight Temporal types, the same-instant/different-zone asymmetry for ZonedDateTime, and Duration's equivalent-forms case (PT60M vs PT1H: equal under .compare() but not .equals()).

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features
    • Added semantic, type-aware ordering for Temporal values in comparisons and sorting.
  • Bug Fixes
    • Fixed gt, gte, lt, lte, and orderBy to use Temporal-native ordering semantics for consistent filtering and sorting (including Duration and ZonedDateTime equivalence by value/instant).
    • Ordering now throws a TypeError for unorderable Temporal types (e.g., PlainMonthDay) and for mixed Temporal types.
  • Tests
    • Expanded Temporal comparison and edge-case coverage, including stable behavior for NaN.

@pkg-pr-new

pkg-pr-new Bot commented May 7, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1519

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1519

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1519

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1519

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1519

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1519

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1519

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1519

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1519

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1519

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1519

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1519

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1519

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1519

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1519

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1519

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1519

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1519

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1519

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1519

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1519

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1519

commit: 14a8923

@obeattie
obeattie marked this pull request as ready for review May 7, 2026 10:37

@kevin-dp kevin-dp 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.

Thank you for your PR. I had a look through it and have some concerns.

Main concern — inconsistency with orderBy

The codebase now has two different orderings for Temporal values:

  • Filter operators (gt/lt/…) → new compareValues → Temporal .compare() (by instant/value).
  • ORDER BYascComparator in comparison.ts:59-65 → lexicographic a.toString() comparison.

These diverge in observable ways:

Case WHERE (new) ORDER BY (existing)
Duration PT1H vs PT60M equal "PT1H" < "PT60M" (wrong)
ZonedDateTime same instant, diff zone equal ordered by zone-name string
PlainMonthDay throws lex-compares fine
mixed Temporal types throws lex-compares fine

A user could write .where(d => gt(d.dur, oneHour)) and .orderBy(d => d.dur) over the same column and get inconsistent semantics — and a PlainMonthDay orderBy "works" while a PlainMonthDay filter throws. Reusing compareValues inside ascComparator would unify them (and incidentally fix the lexicographic Duration bug).

Other concerns

  1. gte/lte behavior change for incomparable primitives (comparison.ts, the a < b ? -1 : a > b ? 1 : 0 fallback). When neither < nor > holds but values aren't truly equal, this returns 0, so gte/lte now return true where the old native a >= b returned false. The concrete case is NaN: previously gte(NaN, NaN) === false, now true (since isUnknown filters only null/undefined, not NaN). gt/lt are unaffected. Not covered by the tests — worth a guard or at least a test pinning the behavior.

  2. a.constructor.compare dispatch works in practice (tests pass on temporal-polyfill, and native Temporal works too), but it's mildly fragile across realms / if constructor is shadowed. Dispatching off the already-computed Symbol.toStringTag would be more robust and consistent with how isTemporal and areValuesEqual already brand-check.

  3. Temporal-vs-non-Temporal operand (e.g. gt(plainDate, "2024-01-15")) falls into the native-operator branch and throws an opaque valueOf TypeError. Same as before this PR, so no regression — but if you're already producing descriptive errors here, it'd be a natural one to catch too.

  4. This PR is missing a changeset

Comment thread packages/db/src/utils/comparison.ts Outdated
obeattie added 5 commits July 1, 2026 11:04
The query compiler's gt/gte/lt/lte evaluators applied JavaScript's native
relational operators directly. That throws TypeError on Temporal values
because Temporal types intentionally make valueOf() throw — a spec-level
guard against silent miscomparison.

Adds a compareValues(a, b) helper that, when both operands share a Temporal
type, dispatches through that type's static .compare() (Instant, PlainDate,
PlainDateTime, PlainTime, PlainYearMonth, ZonedDateTime, Duration). Mixed
Temporal types and types without a defined ordering (PlainMonthDay) throw a
descriptive TypeError rather than fall back to a string-lex pseudo-compare,
keeping us in line with Temporal's design. Non-Temporal values continue to
use native operators (Dates via valueOf, numbers, strings, etc.).

The gt/gte/lt/lte cases in evaluators.ts each switch from `a > b` to
`compareValues(a, b) > 0` (and equivalents). Equality (eq) is unchanged: it
still goes through normalizeValue's tagged toString, which means ZonedDateTime
eq treats the zone as part of identity while ordering compares by instant —
matching .equals() vs .compare() semantics in the spec.

Tests cover all eight Temporal types, the same-instant-different-zone
asymmetry for ZonedDateTime, and Duration's equivalent-forms case (PT60M vs
PT1H — equal under .compare() but not .equals()).
@obeattie
obeattie force-pushed the temporal-comparators branch from 4aa5a51 to c049f08 Compare July 1, 2026 12:48
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89151b6a-59d0-43e1-b492-c9a9881fcf3e

📥 Commits

Reviewing files that changed from the base of the PR and between acba812 and 14a8923.

📒 Files selected for processing (1)
  • .changeset/temporal-comparators.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/temporal-comparators.md

📝 Walkthrough

Walkthrough

This PR adds Temporal-aware comparison helpers, applies them to sorting and gt/gte/lt/lte query operators, and adds tests for Temporal values, mixed types, and NaN behavior.

Changes

Temporal comparison support

Layer / File(s) Summary
Temporal comparison helpers
packages/db/src/utils/comparison.ts
Adds cached Temporal dispatch through static .compare methods, exports compareTemporalValues and compareValues, and updates ascComparator.
Query evaluator integration
packages/db/src/query/compiler/evaluators.ts
Uses compareValues for ordered comparison operators while preserving existing UNKNOWN and isUnorderable handling.
Validation and release metadata
packages/db/tests/comparison.test.ts, packages/db/tests/query/compiler/evaluators.test.ts, .changeset/temporal-comparators.md
Adds Temporal and NaN comparison coverage and documents the patch release.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • TanStack/db#1370: Both changes modify Temporal-aware comparison logic in packages/db/src/utils/comparison.ts.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding Temporal support to query comparison operators in @tanstack/db.
Description check ✅ Passed The description follows the template and covers changes, testing, and release impact, though the release-impact checkbox could better reflect the added changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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.

@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 (2)
packages/db/tests/comparison.test.ts (1)

47-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add one direct ascComparator Temporal regression here too.

This PR also changes the orderBy path in packages/db/src/utils/comparison.ts:83-85, but this file only pins compareValues NaN behavior. A single direct comparator case for a bug this PR fixes—e.g. same-instant/different-zone ZonedDateTime or PT60M vs PT1H—would keep the ascComparator branch covered independently of the evaluator tests. As per coding guidelines, **/*.test.{ts,tsx,js}: Always add unit tests that reproduce a bug before fixing it to ensure the bug is fixed and prevent regression.

Example coverage to add
 import { describe, expect, it } from 'vitest'
+import { Temporal } from 'temporal-polyfill'
 import {
   ascComparator,
   compareValues,
   defaultComparator,
 } from '../src/utils/comparison'
@@
 describe(`compareValues - NaN behavior`, () => {
   // NaN satisfies neither < nor >, so the fallback returns 0. In practice
   // gt/gte/lt/lte catch NaN via isUnorderable before reaching compareValues.
   it(`treats NaN as equal to NaN`, () => {
     expect(compareValues(NaN, NaN)).toBe(0)
   })
@@
   it(`returns 0 for NaN vs a finite number — neither < nor > holds for NaN`, () => {
     expect(compareValues(NaN, 5)).toBe(0)
     expect(compareValues(5, NaN)).toBe(0)
   })
 })
+
+describe(`ascComparator - Temporal behavior`, () => {
+  const opts = DEFAULT_COMPARE_OPTIONS
+
+  it(`treats same-instant ZonedDateTimes in different zones as equal for ordering`, () => {
+    const nyNoon = Temporal.ZonedDateTime.from(`2024-01-15T12:00:00[America/New_York]`)
+    const inTokyo = nyNoon.withTimeZone(`Asia/Tokyo`)
+
+    expect(ascComparator(nyNoon, inTokyo, opts)).toBe(0)
+  })
+})
🤖 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/comparison.test.ts` around lines 47 - 58, Add a direct
regression test for the ascComparator branch in comparison.test.ts, since this
suite currently only covers compareValues NaN behavior. Use a Temporal case that
exercises the orderBy path in comparison.ts, such as two ZonedDateTime values
representing the same instant in different zones or PT60M versus PT1H, and
assert the comparator ordering directly so this bug is covered without relying
only on evaluator tests.

Source: Coding guidelines

packages/db/src/utils/comparison.ts (1)

256-293: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use unknown for the new exported helper parameters.

These helpers are a public comparison choke point; unknown keeps the API safer without changing runtime behavior, and the existing checks can narrow before comparing.

🤖 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/utils/comparison.ts` around lines 256 - 293, The exported
comparison helpers in compareTemporalValues and compareValues currently use any
for their parameters, which weakens the public API. Change the parameter types
to unknown and keep the existing narrowing logic in compareValues and
compareTemporalValues so runtime behavior stays the same while the type surface
is safer.

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/src/utils/comparison.ts`:
- Around line 256-293: The exported comparison helpers in compareTemporalValues
and compareValues currently use any for their parameters, which weakens the
public API. Change the parameter types to unknown and keep the existing
narrowing logic in compareValues and compareTemporalValues so runtime behavior
stays the same while the type surface is safer.

In `@packages/db/tests/comparison.test.ts`:
- Around line 47-58: Add a direct regression test for the ascComparator branch
in comparison.test.ts, since this suite currently only covers compareValues NaN
behavior. Use a Temporal case that exercises the orderBy path in comparison.ts,
such as two ZonedDateTime values representing the same instant in different
zones or PT60M versus PT1H, and assert the comparator ordering directly so this
bug is covered without relying only on evaluator tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8340d19e-902c-48a4-935d-ff795f6c145b

📥 Commits

Reviewing files that changed from the base of the PR and between f7da776 and c049f08.

📒 Files selected for processing (5)
  • .changeset/temporal-comparators.md
  • packages/db/src/query/compiler/evaluators.ts
  • packages/db/src/utils/comparison.ts
  • packages/db/tests/comparison.test.ts
  • packages/db/tests/query/compiler/evaluators.test.ts

@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/comparison.test.ts (1)

48-79: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing negative-path tests for the TypeError behavior.

Per the PR objectives, mixed Temporal types and unorderable types (e.g. PlainMonthDay) should throw a descriptive TypeError from the comparison helpers instead of falling back to string comparison — this is the actual bug being fixed. This file only tests successful ordering (PlainDate, ZonedDateTime) and NaN fallback behavior; it doesn't verify the throw path directly on compareValues/ascComparator.

Since this is the direct unit-test file for these helpers, it's the natural place to assert the throwing behavior (e.g. expect(() => compareValues(plainDate, zonedDateTime)).toThrow(TypeError) and similarly for two PlainMonthDay instances).

As per coding guidelines, **/*.test.{ts,tsx,js}: "Always add unit tests that reproduce a bug before fixing it to ensure the bug is fixed and prevent regression."

✅ Suggested additional tests
 describe(`compareValues - NaN behavior`, () => {
   // NaN satisfies neither < nor >, so the fallback returns 0. In practice
   // gt/gte/lt/lte catch NaN via isUnorderable before reaching compareValues.
   it(`treats NaN as equal to NaN`, () => {
     expect(compareValues(NaN, NaN)).toBe(0)
   })

   it(`returns 0 for NaN vs a finite number — neither < nor > holds for NaN`, () => {
     expect(compareValues(NaN, 5)).toBe(0)
     expect(compareValues(5, NaN)).toBe(0)
   })
 })
+
+describe(`compareValues - unorderable Temporal values`, () => {
+  it(`throws when comparing mixed Temporal types`, () => {
+    const date = Temporal.PlainDate.from(`2024-01-01`)
+    const dateTime = Temporal.PlainDateTime.from(`2024-01-01T00:00:00`)
+    expect(() => compareValues(date, dateTime)).toThrow(TypeError)
+  })
+
+  it(`throws when comparing PlainMonthDay values, which lack a total ordering`, () => {
+    const a = Temporal.PlainMonthDay.from(`01-01`)
+    const b = Temporal.PlainMonthDay.from(`02-02`)
+    expect(() => compareValues(a, b)).toThrow(TypeError)
+  })
+})
🤖 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/comparison.test.ts` around lines 48 - 79, Add negative-path
unit tests in comparison.test.ts for the comparison helpers’ TypeError behavior.
Use compareValues and ascComparator to assert that mixed Temporal types and
unorderable Temporal values (such as PlainMonthDay) throw a descriptive
TypeError instead of falling back to string comparison. Keep the existing
PlainDate, ZonedDateTime, and NaN success cases, and add explicit expect(() =>
...).toThrow(TypeError) coverage for the buggy paths.

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/comparison.test.ts`:
- Around line 48-79: Add negative-path unit tests in comparison.test.ts for the
comparison helpers’ TypeError behavior. Use compareValues and ascComparator to
assert that mixed Temporal types and unorderable Temporal values (such as
PlainMonthDay) throw a descriptive TypeError instead of falling back to string
comparison. Keep the existing PlainDate, ZonedDateTime, and NaN success cases,
and add explicit expect(() => ...).toThrow(TypeError) coverage for the buggy
paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8000d08-afb3-408d-a810-3238518aa7ee

📥 Commits

Reviewing files that changed from the base of the PR and between c049f08 and 45d8640.

📒 Files selected for processing (2)
  • packages/db/src/utils/comparison.ts
  • packages/db/tests/comparison.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/src/utils/comparison.ts

@obeattie
obeattie requested a review from kevin-dp July 1, 2026 13:08
@obeattie

obeattie commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@kevin-dp Thanks! I've made those changes so would appreciate another look.

@kevin-dp kevin-dp 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.

Hi @obeattie,

Thanks for addressing my review!
The PR is good to go now. Could you just extend the changeset according to my comment please.

Comment thread .changeset/temporal-comparators.md Outdated
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.

2 participants