Skip to content

Commit 7b6add2

Browse files
KyleAMathewsclaude
andcommitted
Merge PR #1368 from goatrenterguy (Ben Guericke)
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>
2 parents 962ed2f + 5dd91ac commit 7b6add2

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

packages/db-ivm/tests/utils.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { describe, expect, it } from 'vitest'
22
import { DefaultMap } from '../src/utils.js'
33
import { hash } from '../src/hashing/index.js'
44

5+
// Minimal mock that mimics Temporal objects: Symbol.toStringTag + toString()
6+
// without requiring the temporal-polyfill dependency.
7+
function createTemporalLike(tag: string, value: string) {
8+
return Object.create(null, {
9+
[Symbol.toStringTag]: { value: tag },
10+
toString: { value: () => value },
11+
})
12+
}
13+
514
describe(`DefaultMap`, () => {
615
it(`should return default value for missing keys`, () => {
716
const map = new DefaultMap(() => 0)
@@ -170,6 +179,53 @@ describe(`hash`, () => {
170179
expect(hash1).not.toBe(hash3) // Different dates should have different hash
171180
})
172181

182+
it(`should hash Temporal objects by value`, () => {
183+
const date1 = createTemporalLike(`Temporal.PlainDate`, `2024-01-15`)
184+
const date2 = createTemporalLike(`Temporal.PlainDate`, `2024-01-15`)
185+
const date3 = createTemporalLike(`Temporal.PlainDate`, `2024-06-15`)
186+
187+
const hash1 = hash(date1)
188+
const hash2 = hash(date2)
189+
const hash3 = hash(date3)
190+
191+
expect(typeof hash1).toBe(hashType)
192+
expect(hash1).toBe(hash2) // Same Temporal date should have same hash
193+
expect(hash1).not.toBe(hash3) // Different Temporal dates should have different hash
194+
195+
// Different Temporal types with overlapping string representations should differ
196+
const plainDate = createTemporalLike(`Temporal.PlainDate`, `2024-01-15`)
197+
const plainDateTime = createTemporalLike(
198+
`Temporal.PlainDateTime`,
199+
`2024-01-15T00:00:00`,
200+
)
201+
202+
expect(hash(plainDate)).not.toBe(hash(plainDateTime))
203+
204+
// Other Temporal types should also hash correctly
205+
const time1 = createTemporalLike(`Temporal.PlainTime`, `10:30:00`)
206+
const time2 = createTemporalLike(`Temporal.PlainTime`, `10:30:00`)
207+
const time3 = createTemporalLike(`Temporal.PlainTime`, `14:00:00`)
208+
209+
expect(hash(time1)).toBe(hash(time2))
210+
expect(hash(time1)).not.toBe(hash(time3))
211+
212+
const instant1 = createTemporalLike(
213+
`Temporal.Instant`,
214+
`2024-01-15T00:00:00Z`,
215+
)
216+
const instant2 = createTemporalLike(
217+
`Temporal.Instant`,
218+
`2024-01-15T00:00:00Z`,
219+
)
220+
const instant3 = createTemporalLike(
221+
`Temporal.Instant`,
222+
`2024-06-15T00:00:00Z`,
223+
)
224+
225+
expect(hash(instant1)).toBe(hash(instant2))
226+
expect(hash(instant1)).not.toBe(hash(instant3))
227+
})
228+
173229
it(`should hash RegExp objects`, () => {
174230
const regex1 = /test/g
175231
const regex2 = /test/g

packages/db/tests/query/join.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { beforeEach, describe, expect, test } from 'vitest'
2+
import { Temporal } from 'temporal-polyfill'
23
import {
34
concat,
45
createLiveQueryCollection,
56
eq,
67
gt,
8+
inArray,
79
isNull,
810
isUndefined,
911
lt,
@@ -12,6 +14,7 @@ import {
1214
} from '../../src/query/index.js'
1315
import { createCollection } from '../../src/collection/index.js'
1416
import {
17+
flushPromises,
1518
mockSyncCollectionOptions,
1619
mockSyncCollectionOptionsNoInitialState,
1720
} from '../utils.js'
@@ -2022,6 +2025,80 @@ function createJoinTests(autoIndex: `off` | `eager`): void {
20222025
chainedJoinQuery.toArray.every((r) => r.balance_amount !== undefined),
20232026
).toBe(true)
20242027
})
2028+
2029+
// Regression test for https://github.com/TanStack/db/issues/1367
2030+
// Temporal objects (PlainDate, ZonedDateTime, etc.) have no enumerable own
2031+
// properties, so Object.keys() returns []. Without special handling in the
2032+
// hash function, all Temporal instances produce identical hashes, causing the
2033+
// IVM join Index to treat old and new rows as equal and silently swallow updates.
2034+
test(`join should propagate Temporal field updates through live queries`, async () => {
2035+
type Task = {
2036+
id: number
2037+
name: string
2038+
project_id: number
2039+
dueDate: Temporal.PlainDate
2040+
}
2041+
2042+
type Project = {
2043+
id: number
2044+
name: string
2045+
}
2046+
2047+
const taskCollection = createCollection(
2048+
mockSyncCollectionOptions<Task>({
2049+
id: `test-temporal-join-${autoIndex}`,
2050+
getKey: (task) => task.id,
2051+
initialData: [
2052+
{
2053+
id: 1,
2054+
name: `Task A`,
2055+
project_id: 10,
2056+
dueDate: Temporal.PlainDate.from(`2024-01-15`),
2057+
},
2058+
],
2059+
autoIndex,
2060+
}),
2061+
)
2062+
2063+
const projectCollection = createCollection(
2064+
mockSyncCollectionOptions<Project>({
2065+
id: `test-temporal-join-projects-${autoIndex}`,
2066+
getKey: (project) => project.id,
2067+
initialData: [{ id: 10, name: `Project Alpha` }],
2068+
autoIndex,
2069+
}),
2070+
)
2071+
2072+
const liveQuery = createLiveQueryCollection({
2073+
startSync: true,
2074+
query: (q) =>
2075+
q
2076+
.from({ task: taskCollection })
2077+
.where(({ task }) => inArray(task.id, [1]))
2078+
.innerJoin({ project: projectCollection }, ({ task, project }) =>
2079+
eq(task.project_id, project.id),
2080+
)
2081+
.select(({ task, project }) => ({
2082+
task,
2083+
project,
2084+
})),
2085+
})
2086+
2087+
await liveQuery.preload()
2088+
expect(liveQuery.toArray).toHaveLength(1)
2089+
expect(
2090+
(liveQuery.toArray[0]!.task.dueDate as Temporal.PlainDate).toString(),
2091+
).toBe(`2024-01-15`)
2092+
2093+
taskCollection.update(1, (draft) => {
2094+
;(draft as any).dueDate = Temporal.PlainDate.from(`2024-06-15`)
2095+
})
2096+
await flushPromises()
2097+
2098+
expect(
2099+
(liveQuery.toArray[0]!.task.dueDate as Temporal.PlainDate).toString(),
2100+
).toBe(`2024-06-15`)
2101+
})
20252102
}
20262103

20272104
describe(`Query JOIN Operations`, () => {

0 commit comments

Comments
 (0)