Skip to content

fix(db, db-ivm): support Temporal objects in join hashing and normalization#1370

Open
KyleAMathews wants to merge 5 commits intomainfrom
fix/temporal-join-hashing
Open

fix(db, db-ivm): support Temporal objects in join hashing and normalization#1370
KyleAMathews wants to merge 5 commits intomainfrom
fix/temporal-join-hashing

Conversation

@KyleAMathews
Copy link
Collaborator

@KyleAMathews KyleAMathews commented Mar 13, 2026

Temporal objects (e.g. Temporal.PlainDate) break live query updates when joins are involved. Simple queries without joins work fine.

Incorporates #1368 by @goatrenterguy (Ben Guericke) which provides the core hash fix and tests.

Root Cause

The structural hash function in db-ivm uses Object.keys() to hash objects. Temporal objects store data in non-enumerable internal slots, so Object.keys() returns [] — every Temporal value hashes identically. When a Temporal field changes on a row in a join index, hash(oldRow) === hash(newRow), the -1 and +1 multiplicities cancel out, and the update is silently swallowed.

Approach

Three related gaps in Temporal support along the join/query pipeline:

1. Hashing (db-ivm) — The primary bug (from #1368). Detect Temporal objects via Symbol.toStringTag and hash by TEMPORAL_MARKER + typeTag + toString() instead of falling through to hashPlainObject. Follows the same pattern used for Date (marker + value representation).

2. Value normalization (db)normalizeValue() converts special types to primitives for Map key equality in join key matching. Added Temporal → string conversion (__temporal__${tag}__${toString}), following the existing Date → getTime() and Uint8Array → __u8__ patterns.

3. Comparator (db)ascComparator() had no Temporal handling, so ORDER BY on Temporal fields would sort by arbitrary object reference IDs. Added string-based comparison via toString(), which produces lexicographically sortable ISO 8601 strings for date/time types.

Also made isTemporal() defensive (null guard on the exported API) and converted the temporalTypes list from Array to Set for consistency with the db-ivm copy.

Key Invariants

  • Different Temporal values must produce different hashes
  • Same Temporal value (different instances) must produce the same hash
  • Temporal join keys must match across both sides of a join via Map lookup
  • Temporal sort order must be chronological, not reference-based

Non-goals

  • Deduplicating the temporalTypes/isTemporal definitions across db and db-ivm packages (would require cross-package dependency changes)
  • Handling Temporal.Duration sort ordering (durations aren't lexicographically sortable via toString() — a separate concern)

Verification

pnpm run build
pnpm --filter @tanstack/db-ivm test -- --run   # 322 tests pass
pnpm --filter @tanstack/db test -- --run        # 2082 tests pass

Files Changed

File Change
packages/db-ivm/src/hashing/hash.ts Add TEMPORAL_MARKER, temporalTypes, isTemporal(), hashTemporal()
packages/db-ivm/tests/utils.test.ts Unit tests for Temporal hash correctness (from #1368)
packages/db/tests/query/join.test.ts Regression test for Temporal join updates (from #1368)
packages/db/src/utils/comparison.ts Add Temporal branch in normalizeValue() and ascComparator()
packages/db/src/utils.ts Add null guard to isTemporal(), convert temporalTypes to Set
.changeset/fix-temporal-join-hashing.md Patch changeset for both packages

Fixes #1367
Incorporates #1368

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Temporal date/time objects (PlainDate, PlainDateTime, PlainTime, Instant) now hash, normalize, compare, and sort correctly in joins so live query updates to Temporal fields propagate reliably.
  • Tests
    • Added regression tests covering Temporal hashing, comparison, join behavior, and live query update propagation.
  • Chores
    • Added a changeset documenting the Temporal-related fixes.

goatrenterguy and others added 2 commits March 13, 2026 11:36
Temporal objects (PlainDate, ZonedDateTime, etc.) have no enumerable own
properties, so Object.keys() returns [] and all instances produce
identical hashes. This causes the IVM join Index to treat old and new
rows as equal, silently swallowing updates when only a Temporal field
changed.

Hash Temporal objects by their Symbol.toStringTag type and toString()
representation to produce correct, value-based hashes.

Fixes #1367

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…zation

Temporal objects have no enumerable properties, so hashPlainObject()
produced identical hashes for all Temporal values. This caused join
index updates to be silently swallowed when a Temporal field changed.

- Add Temporal-aware hashing via Symbol.toStringTag + toString()
- Add Temporal normalization in normalizeValue() for join key matching
- Add Temporal handling in ascComparator for correct sort ordering
- Add null guard to exported isTemporal()
- Convert temporalTypes from Array to Set for consistency

Fixes #1367

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 13, 2026

🦋 Changeset detected

Latest commit: bcf3ef5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@tanstack/db Patch
@tanstack/db-ivm Patch
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/query-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds Temporal-aware hashing, normalization, and comparison so Temporal objects are hashed and compared by their tag + string representation, ensuring Temporal field changes produce distinct hashes and propagate through IVM join indexes and live queries.

Changes

Cohort / File(s) Summary
Changeset Documentation
\.changeset/fix-temporal-join-hashing.md
New changeset describing the fix for Temporal objects causing join live-update failures.
Hashing (db-ivm)
packages/db-ivm/src/hashing/hash.ts
Adds Temporal detection (TEMPORAL_MARKER), isTemporal routing, and hashTemporal() to hash Temporal values by tag+string instead of enumerable keys.
Temporal Utilities
packages/db/src/utils.ts
Introduces TemporalLike interface, switches temporal type list to a Set, replaces getStringTag with direct Symbol.toStringTag access, and updates isTemporal to a type guard.
Comparison & Normalization
packages/db/src/utils/comparison.ts
Uses isTemporal to compare Temporal values via toString() in ascComparator and normalizes Temporal values as __temporal__{tag}:{toString()}.
Tests — Hashing
packages/db-ivm/tests/utils.test.ts
Adds tests (with temporal-polyfill) verifying Temporal types/values produce stable, distinct hashes and differ from non-Temporal values.
Tests — Join Regression
packages/db/tests/query/join.test.ts
Adds regression test (imports temporal-polyfill, inArray, flushPromises) ensuring Temporal field updates propagate through live join queries.
Dev Dependency
packages/db-ivm/package.json
Adds temporal-polyfill to devDependencies for test support.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇
I sniffed the tags where quiet dates lay,
No keys to crunch — they once slipped away.
I learned their names, I gave them string and stamp,
Now joins stir awake and no update is damp.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 clearly and concisely summarizes the main change: adding Temporal object support to join hashing and normalization across db and db-ivm packages.
Description check ✅ Passed The description is comprehensive and well-structured, covering root cause analysis, approach across three areas, key invariants, verification steps, and file changes. All required template sections are addressed appropriately.
Linked Issues check ✅ Passed All objectives from issue #1367 are fully addressed: Temporal detection via Symbol.toStringTag [hash.ts], value normalization for Map key equality [comparison.ts], chronological sort order handling [comparison.ts], and invariant preservation verified by tests.
Out of Scope Changes check ✅ Passed All changes are directly in scope of the linked issues. The PR adds Temporal support to hashing, normalization, and comparators with corresponding tests and changeset. No unrelated modifications detected.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/temporal-join-hashing
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 13, 2026

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: d3f1e9e

@github-actions
Copy link
Contributor

github-actions bot commented Mar 13, 2026

Size Change: +97 B (+0.1%)

Total Size: 98.7 kB

Filename Size Change
./packages/db/dist/esm/utils.js 927 B +3 B (+0.32%)
./packages/db/dist/esm/utils/comparison.js 1.05 kB +94 B (+9.87%) ⚠️
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.22 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.75 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/subscription.js 3.71 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.83 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.74 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 B
./packages/db/dist/esm/local-only.js 808 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/proxy.js 3.75 kB
./packages/db/dist/esm/query/builder/functions.js 733 B
./packages/db/dist/esm/query/builder/index.js 4.1 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.43 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 2.23 kB
./packages/db/dist/esm/query/compiler/index.js 2.05 kB
./packages/db/dist/esm/query/compiler/joins.js 2.11 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/compiler/select.js 1.09 kB
./packages/db/dist/esm/query/effect.js 4.78 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.31 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 1.94 kB
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/live/utils.js 1.36 kB
./packages/db/dist/esm/query/optimizer.js 2.62 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/query-once.js 359 B
./packages/db/dist/esm/query/subset-dedupe.js 927 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.3 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Mar 13, 2026

Size Change: 0 B

Total Size: 4.23 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 249 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/db-ivm/src/hashing/hash.ts (1)

23-32: Consider centralizing Temporal type tags to avoid cross-package drift.
Line 23-Line 32 duplicates the same Temporal tag registry already present in packages/db/src/utils.ts; keeping one source of truth reduces future mismatch risk.

As per coding guidelines: Extract common logic into reusable utility functions when duplicated across multiple places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db-ivm/src/hashing/hash.ts` around lines 23 - 32, The local
temporalTypes Set duplicates the Temporal tag registry; remove this local
declaration and import the single shared constant from the common utils module
(export it as a stable symbol such as TEMPORAL_TYPE_TAGS or getTemporalTypeTags)
so both packages use the same source of truth; update this file to reference
that imported symbol (replacing temporalTypes) and ensure the shared utils
exports are typed and tested for consumers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/db-ivm/src/hashing/hash.ts`:
- Around line 34-37: The current isTemporal uses unsafe "as any" casts; define a
TemporalLike interface/type that includes an optional [Symbol.toStringTag]:
string and any Temporal-specific properties needed, change isTemporal to the
type guard signature "function isTemporal(input: unknown): input is
TemporalLike" (or "input is TemporalLike") and implement it by reading (input as
TemporalLike)[Symbol.toStringTag] and checking temporalTypes, then remove all
uses of "as any" in hashTemporal and elsewhere by relying on the type guard to
narrow input to TemporalLike before accessing Temporal fields; update
hashTemporal to accept the narrowed type rather than casting.

In `@packages/db/src/utils/comparison.ts`:
- Around line 58-65: The Temporal branch currently compares by toString(), which
can misorder values (especially ZonedDateTime across zones) and does not enforce
same-type comparison; change it so when isTemporal(a) && isTemporal(b) and they
are the same Temporal type (e.g., a.constructor === b.constructor or compare
constructor names), call the appropriate Temporal compare method for that type
(e.g., Temporal.ZonedDateTime.compare(a,b), Temporal.PlainDateTime.compare(a,b),
Temporal.PlainDate.compare(a,b), Temporal.PlainTime.compare(a,b), etc.) and
return the compare result normalized to -1/0/1; if the types differ fall back to
the existing non-Temporal logic (or a stable tie-breaker) rather than comparing
toString().

---

Nitpick comments:
In `@packages/db-ivm/src/hashing/hash.ts`:
- Around line 23-32: The local temporalTypes Set duplicates the Temporal tag
registry; remove this local declaration and import the single shared constant
from the common utils module (export it as a stable symbol such as
TEMPORAL_TYPE_TAGS or getTemporalTypeTags) so both packages use the same source
of truth; update this file to reference that imported symbol (replacing
temporalTypes) and ensure the shared utils exports are typed and tested for
consumers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79560193-361d-49f8-ad69-09528e1e8ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 1270156 and 962ed2f.

📒 Files selected for processing (4)
  • .changeset/fix-temporal-join-hashing.md
  • packages/db-ivm/src/hashing/hash.ts
  • packages/db/src/utils.ts
  • packages/db/src/utils/comparison.ts

Comment on lines +58 to +65
// If both are Temporal objects of the same type, compare by string representation
if (isTemporal(a) && isTemporal(b)) {
const aStr = a.toString()
const bStr = b.toString()
if (aStr < bStr) return -1
if (aStr > bStr) return 1
return 0
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use Temporal compare semantics instead of raw toString() ordering.
Line 58-Line 65 can misorder chronological values (e.g., Temporal.ZonedDateTime across different zones/offsets). It also doesn’t enforce same-type comparison despite the comment.

💡 Proposed fix
   // If both are Temporal objects of the same type, compare by string representation
   if (isTemporal(a) && isTemporal(b)) {
-    const aStr = a.toString()
-    const bStr = b.toString()
-    if (aStr < bStr) return -1
-    if (aStr > bStr) return 1
-    return 0
+    const aTag = a[Symbol.toStringTag]
+    const bTag = b[Symbol.toStringTag]
+
+    // Prefer Temporal's native compare when both values share the same constructor/type.
+    if (aTag === bTag && a.constructor === b.constructor) {
+      const compare = (a.constructor as { compare?: (x: unknown, y: unknown) => number }).compare
+      if (typeof compare === `function`) {
+        return compare(a, b)
+      }
+    }
+
+    // Deterministic fallback for mixed Temporal types or missing compare().
+    if (aTag < bTag) return -1
+    if (aTag > bTag) return 1
+    const aStr = a.toString()
+    const bStr = b.toString()
+    if (aStr < bStr) return -1
+    if (aStr > bStr) return 1
+    return 0
   }
For JavaScript Temporal, can lexicographic ordering of `toString()` differ from `Temporal.ZonedDateTime.compare(a, b)` for values in different time zones/offsets? Provide a concrete example.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/src/utils/comparison.ts` around lines 58 - 65, The Temporal
branch currently compares by toString(), which can misorder values (especially
ZonedDateTime across zones) and does not enforce same-type comparison; change it
so when isTemporal(a) && isTemporal(b) and they are the same Temporal type
(e.g., a.constructor === b.constructor or compare constructor names), call the
appropriate Temporal compare method for that type (e.g.,
Temporal.ZonedDateTime.compare(a,b), Temporal.PlainDateTime.compare(a,b),
Temporal.PlainDate.compare(a,b), Temporal.PlainTime.compare(a,b), etc.) and
return the compare result normalized to -1/0/1; if the types differ fall back to
the existing non-Temporal logic (or a stable tie-breaker) rather than comparing
toString().

Incorporates Ben's Temporal hash fix with tests:
- Unit tests for Temporal hash correctness (mock-based)
- Regression test for join live query with Temporal field updates
- Removed duplicate changeset (ours covers both packages)
- Resolved hash.ts conflict: kept Set-based temporalTypes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

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 (1)
packages/db/tests/query/join.test.ts (1)

2093-2095: Remove any type cast from update callback.

The dueDate property is defined on the Task type (line 2035), so the as any cast is unnecessary. Use explicit typing on the callback parameter instead:

Suggested fix
-    taskCollection.update(1, (draft) => {
-      ;(draft as any).dueDate = Temporal.PlainDate.from(`2024-06-15`)
-    })
+    taskCollection.update(1, (draft: Task) => {
+      draft.dueDate = Temporal.PlainDate.from(`2024-06-15`)
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/tests/query/join.test.ts` around lines 2093 - 2095, Remove the
unnecessary "as any" cast in the update callback for taskCollection.update;
instead give the callback a proper typed parameter (e.g., (draft: Task) => {
draft.dueDate = Temporal.PlainDate.from('2024-06-15') }) so the compiler knows
draft has dueDate; reference the Task type (declared earlier) and the
taskCollection.update call to locate and replace the "(draft as any)" usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/db-ivm/tests/utils.test.ts`:
- Around line 7-12: The function createTemporalLike returns an untyped object
from Object.create which becomes any; add an explicit return type such as {
toString(): string; [Symbol.toStringTag]: string } to the function signature so
TypeScript enforces the shape (e.g. change function createTemporalLike(tag:
string, value: string): { toString(): string; [Symbol.toStringTag]: string } {
... }), referencing createTemporalLike, Symbol.toStringTag and the toString
property in the change.

---

Nitpick comments:
In `@packages/db/tests/query/join.test.ts`:
- Around line 2093-2095: Remove the unnecessary "as any" cast in the update
callback for taskCollection.update; instead give the callback a proper typed
parameter (e.g., (draft: Task) => { draft.dueDate =
Temporal.PlainDate.from('2024-06-15') }) so the compiler knows draft has
dueDate; reference the Task type (declared earlier) and the
taskCollection.update call to locate and replace the "(draft as any)" usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e94ee10-c120-4dde-8965-f4adc304ef4f

📥 Commits

Reviewing files that changed from the base of the PR and between 962ed2f and 7b6add2.

📒 Files selected for processing (2)
  • packages/db-ivm/tests/utils.test.ts
  • packages/db/tests/query/join.test.ts

Copy link
Collaborator

@samwillis samwillis left a comment

Choose a reason for hiding this comment

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

Looks like we don't have the types for Temporal in the typescript setup. We should add them rather than cast as any.

Address review feedback from Sam Willis and CodeRabbit:
- Add TemporalLike interface in db-ivm for type-safe Temporal detection
- Make isTemporal a proper type guard (returns input is TemporalLike)
- Remove as-any casts in hashTemporal
- Add return type to createTemporalLike test helper
- Remove unnecessary casts in join regression test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

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/query/join.test.ts (1)

2094-2097: Strengthen post-update assertions to catch stale/duplicate rows.

After the update, the test validates only the first row’s date. Add explicit cardinality and stale-value checks so duplicate/stale join rows can’t pass silently.

✅ Suggested test hardening
     await flushPromises()
-
-    expect(String(liveQuery.toArray[0]!.task.dueDate)).toBe(`2024-06-15`)
+    const rowsAfterUpdate = liveQuery.toArray
+    expect(rowsAfterUpdate).toHaveLength(1)
+    expect(String(rowsAfterUpdate[0]!.task.dueDate)).toBe(`2024-06-15`)
+    expect(
+      rowsAfterUpdate.some(
+        (row) => String(row.task.dueDate) === `2024-01-15`,
+      ),
+    ).toBe(false)

As per coding guidelines, "Test corner cases including: empty collections, single elements, undefined vs null, resolved promises, race conditions, limit/offset edge cases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/tests/query/join.test.ts` around lines 2094 - 2097, The assertion
after await flushPromises() only checks the first element and can miss
stale/duplicate join rows; update the test that references liveQuery and its
toArray result to assert the expected cardinality (e.g., exact length), verify
each element’s task.dueDate equals the expected value (`2024-06-15`), and ensure
there are no duplicate task ids or stale values (compare task.id or serialize
each row to confirm uniqueness) so stale/duplicate join rows cannot pass
silently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/db/tests/query/join.test.ts`:
- Around line 2094-2097: The assertion after await flushPromises() only checks
the first element and can miss stale/duplicate join rows; update the test that
references liveQuery and its toArray result to assert the expected cardinality
(e.g., exact length), verify each element’s task.dueDate equals the expected
value (`2024-06-15`), and ensure there are no duplicate task ids or stale values
(compare task.id or serialize each row to confirm uniqueness) so stale/duplicate
join rows cannot pass silently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4b4060a-9f27-4542-a67a-8e7a1a85772a

📥 Commits

Reviewing files that changed from the base of the PR and between 7b6add2 and bfea297.

📒 Files selected for processing (4)
  • packages/db-ivm/src/hashing/hash.ts
  • packages/db-ivm/tests/utils.test.ts
  • packages/db/src/utils.ts
  • packages/db/tests/query/join.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db-ivm/src/hashing/hash.ts
  • packages/db-ivm/tests/utils.test.ts

Add temporal-polyfill as devDependency to db-ivm and replace
createTemporalLike mocks with real Temporal.PlainDate, PlainTime,
PlainDateTime, and Instant objects in hash tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KyleAMathews KyleAMathews requested a review from samwillis March 13, 2026 20:38
Copy link

@coderabbitai coderabbitai bot left a comment

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-ivm/tests/utils.test.ts (1)

174-207: Thorough test coverage for Temporal hashing invariants.

The tests correctly validate:

  • Same Temporal values produce identical hashes (lines 184, 198, 205)
  • Different Temporal values produce different hashes (lines 185, 199, 206)
  • Different Temporal types with overlapping ISO strings produce distinct hashes (line 191) — critical for preventing collisions between PlainDate and PlainDateTime

Consider adding an edge case test to verify Temporal objects hash differently from plain strings with identical representations:

🧪 Optional: Add Temporal vs string collision test
       expect(hash(instant1)).not.toBe(hash(instant3))
+
+      // Temporal objects should hash differently from plain strings with same representation
+      const temporalDate = Temporal.PlainDate.from(`2024-01-15`)
+      const plainString = `2024-01-15`
+      expect(hash(temporalDate)).not.toBe(hash(plainString))
     })

Based on learnings: "Test corner cases including: empty collections, single elements, undefined vs null..."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db-ivm/tests/utils.test.ts` around lines 174 - 207, Add a test
asserting Temporal objects do not collide with plain strings that share their
ISO representation: in the same test block that exercises
hash(Temporal.PlainDate), hash(Temporal.PlainDateTime), hash(Temporal.Instant),
add assertions that hash(Temporal.PlainDate.from("2024-01-15")) !==
hash("2024-01-15") and similarly for Temporal.PlainDateTime/Temporal.Instant
compared to their ISO string forms using the existing hash function to ensure
Temporal types are distinguished from raw strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/db-ivm/tests/utils.test.ts`:
- Around line 174-207: Add a test asserting Temporal objects do not collide with
plain strings that share their ISO representation: in the same test block that
exercises hash(Temporal.PlainDate), hash(Temporal.PlainDateTime),
hash(Temporal.Instant), add assertions that
hash(Temporal.PlainDate.from("2024-01-15")) !== hash("2024-01-15") and similarly
for Temporal.PlainDateTime/Temporal.Instant compared to their ISO string forms
using the existing hash function to ensure Temporal types are distinguished from
raw strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93ce6a03-8f7e-46cc-bf89-fbefeb20d8fc

📥 Commits

Reviewing files that changed from the base of the PR and between bfea297 and bcf3ef5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/db-ivm/package.json
  • packages/db-ivm/tests/utils.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.

Temporal dates break live query updates when using joins

3 participants