Describe the bug
MultiSet.consolidate() merges keyed records whose keys or primitive values differ only in type. A number and a string with the same text, such as 1 and "1", become one identity. Multiplicities for two different records are then summed together. A retraction of one record can erase an insert of a different record.
The keyed path builds a string identity. getStringId in packages/db-ivm/src/utils.ts encodes every primitive as `str_${String(value)}`, and consolidate joins the key the same way:
getStringId(value: any): string {
if (value === null) return `null`
if (value === undefined) return `undefined`
if (typeof value !== `object`) return `str_${String(value)}`
return `obj_${this.getId(value)}`
}
// ...
const compositeKey = key + `|` + valueId
So 1 and "1" both produce str_1, and the keys 1 and "1" both produce 1|.
To Reproduce
import { MultiSet } from '@tanstack/db-ivm'
new MultiSet<any>([[[1, 'v'], 1], [['1', 'v'], 1]]).consolidate().getInner()
// [[[1, 'v'], 2]] expected two records, each with multiplicity 1
new MultiSet<any>([[['k', 1], 1], [['k', '1'], -1]]).consolidate().getInner()
// [] expected [[['k', 1], 1], [['k', '1'], -1]]
new MultiSet<any>([[['k', [1, null]], 1], [['k', ['1', null]], -1]]).consolidate().getInner()
// [] expected both records (join tuples have the same collision)
The unkeyed path is correct:
new MultiSet<any>([[1, 1], ['1', -1]]).consolidate().getInner()
// [[1, 1], ['1', -1]]
Expected behavior
Keyed consolidation should keep the identity rule it documents. Keys compare by value, and values compare by reference. A number and a string are different values, so records that differ only in that type must stay separate.
Scope
-
Object values are compared by reference and are not affected. The collision needs a primitive key or a primitive value, or a join tuple that holds a primitive.
-
In @tanstack/db, the collision needs one of these:
- one collection with numeric and string keys that print the same, such as
1 and "1";
- a keyed stream that carries primitive values.
I have not traced which query shapes produce such a stream.
-
A code-weight change to MultiSet.consolidate is in progress. It keeps this behavior unchanged on purpose. Its consolidation oracle excludes these cross-type values from its grammar and links to this issue. A fix should remove that exclusion.
Suggested fix and test
- Include the type in the primitive identity, for example
`${typeof value}:${String(value)}`. Also encode the key with its type before the | join.
- Remove the exclusion from the consolidation oracle, and add these cases as witnesses. Each case should fail before the fix and pass after it.
- Add a changeset for
@tanstack/db-ivm.
origin/mainatb5d92ceb)Describe the bug
MultiSet.consolidate()merges keyed records whose keys or primitive values differ only in type. A number and a string with the same text, such as1and"1", become one identity. Multiplicities for two different records are then summed together. A retraction of one record can erase an insert of a different record.The keyed path builds a string identity.
getStringIdinpackages/db-ivm/src/utils.tsencodes every primitive as`str_${String(value)}`, andconsolidatejoins the key the same way:So
1and"1"both producestr_1, and the keys1and"1"both produce1|.To Reproduce
The unkeyed path is correct:
Expected behavior
Keyed consolidation should keep the identity rule it documents. Keys compare by value, and values compare by reference. A number and a string are different values, so records that differ only in that type must stay separate.
Scope
Object values are compared by reference and are not affected. The collision needs a primitive key or a primitive value, or a join tuple that holds a primitive.
In
@tanstack/db, the collision needs one of these:1and"1";I have not traced which query shapes produce such a stream.
A code-weight change to
MultiSet.consolidateis in progress. It keeps this behavior unchanged on purpose. Its consolidation oracle excludes these cross-type values from its grammar and links to this issue. A fix should remove that exclusion.Suggested fix and test
`${typeof value}:${String(value)}`. Also encode the key with its type before the|join.@tanstack/db-ivm.