Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brave-rocks-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tanstack/db-ivm": patch
"@tanstack/db": patch
---

Add support for Date objects to min/max aggregates and range queries when using an index.
32 changes: 18 additions & 14 deletions packages/db-ivm/src/operators/groupBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,21 @@ export function avg<T>(
* Creates a min aggregate function that computes the minimum value in a group
* @param valueExtractor Function to extract a numeric value from each data entry
*/
export function min<T>(
valueExtractor: (value: T) => number = (v) => v as unknown as number
): AggregateFunction<T, number, number> {
export function min<T, V extends number | Date | bigint>(
valueExtractor: (value: T) => V = (v) => v as unknown as V
): AggregateFunction<T, V, V> {
return {
preMap: (data: T) => valueExtractor(data),
reduce: (values: Array<[number, number]>) => {
let minValue = Number.POSITIVE_INFINITY
reduce: (values) => {
let minValue: V | undefined
for (const [value, _multiplicity] of values) {
if (value < minValue) {
if (minValue && value < minValue) {
minValue = value
} else if (!minValue) {
minValue = value
}
}
return minValue === Number.POSITIVE_INFINITY ? 0 : minValue
return minValue ?? (0 as V)
},
}
}
Expand All @@ -235,19 +237,21 @@ export function min<T>(
* Creates a max aggregate function that computes the maximum value in a group
* @param valueExtractor Function to extract a numeric value from each data entry
*/
export function max<T>(
valueExtractor: (value: T) => number = (v) => v as unknown as number
): AggregateFunction<T, number, number> {
export function max<T, V extends number | Date | bigint>(
valueExtractor: (value: T) => V = (v) => v as unknown as V
): AggregateFunction<T, V, V> {
return {
preMap: (data: T) => valueExtractor(data),
reduce: (values: Array<[number, number]>) => {
let maxValue = Number.NEGATIVE_INFINITY
reduce: (values) => {
let maxValue: V | undefined
for (const [value, _multiplicity] of values) {
if (value > maxValue) {
if (maxValue && value > maxValue) {
maxValue = value
} else if (!maxValue) {
maxValue = value
}
}
return maxValue === Number.NEGATIVE_INFINITY ? 0 : maxValue
return maxValue ?? (0 as V)
},
}
}
Expand Down
17 changes: 12 additions & 5 deletions packages/db-ivm/tests/operators/groupBy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,13 +524,16 @@ describe(`Operators`, () => {
const input = graph.newInput<{
category: string
amount: number
date: Date
}>()
let latestMessage: any = null

input.pipe(
groupBy((data) => ({ category: data.category }), {
minimum: min((data) => data.amount),
maximum: max((data) => data.amount),
min_date: min((data) => data.date),
max_date: max((data) => data.date),
}),
output((message) => {
latestMessage = message
Expand All @@ -542,11 +545,11 @@ describe(`Operators`, () => {
// Initial data
input.sendData(
new MultiSet([
[{ category: `A`, amount: 10 }, 1],
[{ category: `A`, amount: 20 }, 1],
[{ category: `A`, amount: 5 }, 1],
[{ category: `B`, amount: 30 }, 1],
[{ category: `B`, amount: 15 }, 1],
[{ category: `A`, amount: 10, date: new Date(`2025/12/13`) }, 1],
[{ category: `A`, amount: 20, date: new Date(`2025/12/15`) }, 1],
[{ category: `A`, amount: 5, date: new Date(`2025/12/12`) }, 1],
[{ category: `B`, amount: 30, date: new Date(`2025/12/12`) }, 1],
[{ category: `B`, amount: 15, date: new Date(`2025/12/13`) }, 1],
])
)

Expand All @@ -563,6 +566,8 @@ describe(`Operators`, () => {
category: `A`,
minimum: 5,
maximum: 20,
min_date: new Date(`2025/12/12`),
max_date: new Date(`2025/12/15`),
},
],
1,
Expand All @@ -574,6 +579,8 @@ describe(`Operators`, () => {
category: `B`,
minimum: 15,
maximum: 30,
min_date: new Date(`2025/12/12`),
max_date: new Date(`2025/12/13`),
},
],
1,
Expand Down
47 changes: 34 additions & 13 deletions packages/db/src/indexes/btree-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,18 @@ export class BTreeIndex<
)
}

// Normalize the value for Map key usage
const normalizedValue = this.normalizeMapKey(indexedValue)

// Check if this value already exists
if (this.valueMap.has(indexedValue)) {
if (this.valueMap.has(normalizedValue)) {
// Add to existing set
this.valueMap.get(indexedValue)!.add(key)
this.valueMap.get(normalizedValue)!.add(key)
} else {
// Create new set for this value
const keySet = new Set<TKey>([key])
this.valueMap.set(indexedValue, keySet)
this.orderedEntries.set(indexedValue, undefined)
this.valueMap.set(normalizedValue, keySet)
this.orderedEntries.set(normalizedValue, undefined)
}

this.indexedKeys.add(key)
Expand All @@ -101,16 +104,19 @@ export class BTreeIndex<
return
}

if (this.valueMap.has(indexedValue)) {
const keySet = this.valueMap.get(indexedValue)!
// Normalize the value for Map key usage
const normalizedValue = this.normalizeMapKey(indexedValue)

if (this.valueMap.has(normalizedValue)) {
const keySet = this.valueMap.get(normalizedValue)!
keySet.delete(key)

// If set is now empty, remove the entry entirely
if (keySet.size === 0) {
this.valueMap.delete(indexedValue)
this.valueMap.delete(normalizedValue)

// Remove from ordered entries
this.orderedEntries.delete(indexedValue)
this.orderedEntries.delete(normalizedValue)
}
}

Expand Down Expand Up @@ -195,7 +201,8 @@ export class BTreeIndex<
* Performs an equality lookup
*/
equalityLookup(value: any): Set<TKey> {
return new Set(this.valueMap.get(value) ?? [])
const normalizedValue = this.normalizeMapKey(value)
return new Set(this.valueMap.get(normalizedValue) ?? [])
}

/**
Expand All @@ -206,8 +213,10 @@ export class BTreeIndex<
const { from, to, fromInclusive = true, toInclusive = true } = options
const result = new Set<TKey>()

const fromKey = from ?? this.orderedEntries.minKey()
const toKey = to ?? this.orderedEntries.maxKey()
const normalizedFrom = this.normalizeMapKey(from)
const normalizedTo = this.normalizeMapKey(to)
const fromKey = normalizedFrom ?? this.orderedEntries.minKey()
const toKey = normalizedTo ?? this.orderedEntries.maxKey()

this.orderedEntries.forRange(
fromKey,
Expand Down Expand Up @@ -240,7 +249,7 @@ export class BTreeIndex<
const keysInResult: Set<TKey> = new Set()
const result: Array<TKey> = []
const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
let key = from
let key = this.normalizeMapKey(from)

while ((key = nextKey(key)) && result.length < n) {
const keys = this.valueMap.get(key)
Expand All @@ -266,7 +275,8 @@ export class BTreeIndex<
const result = new Set<TKey>()

for (const value of values) {
const keys = this.valueMap.get(value)
const normalizedValue = this.normalizeMapKey(value)
const keys = this.valueMap.get(normalizedValue)
if (keys) {
keys.forEach((key) => result.add(key))
}
Expand All @@ -289,4 +299,15 @@ export class BTreeIndex<
get valueMapData(): Map<any, Set<TKey>> {
return this.valueMap
}

/**
* Normalizes values for use as Map keys to ensure proper equality and range queries
* For Date objects, uses timestamp.
*/
private normalizeMapKey(value: any): any {
if (value instanceof Date) {
return value.valueOf()
}
return value
}
}
6 changes: 3 additions & 3 deletions packages/db/src/query/builder/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ type ExtractType<T> =
// Helper type to determine aggregate return type based on input nullability
type AggregateReturnType<T> =
ExtractType<T> extends infer U
? U extends number | undefined | null
? U extends number | undefined | null | Date | bigint
? Aggregate<U>
: Aggregate<number | undefined | null>
: Aggregate<number | undefined | null>
: Aggregate<number | undefined | null | Date | bigint>
: Aggregate<number | undefined | null | Date | bigint>

// Helper type to determine string function return type based on input nullability
type StringFunctionReturnType<T> =
Expand Down
3 changes: 3 additions & 0 deletions packages/db/src/query/compiler/evaluators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
return (data) => {
const a = argA(data)
const b = argB(data)
if (a instanceof Date && b instanceof Date) {
return a.valueOf() == b.valueOf()
}
return a === b
}
}
Expand Down
18 changes: 16 additions & 2 deletions packages/db/src/query/compiler/group-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ function getAggregateFunction(aggExpr: Aggregate) {
return typeof value === `number` ? value : value != null ? Number(value) : 0
}

// Create a value extractor function for the expression to aggregate
const valueExtractorWithDate = ([, namespacedRow]: [
string,
NamespacedRow,
]) => {
const value = compiledExpr(namespacedRow)
// Ensure we return a number for numeric aggregate functions
return typeof value === `number` || value instanceof Date
? value
: value != null
? Number(value)
: 0
}

// Create a raw value extractor function for the expression to aggregate
const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
return compiledExpr(namespacedRow)
Expand All @@ -363,9 +377,9 @@ function getAggregateFunction(aggExpr: Aggregate) {
case `avg`:
return avg(valueExtractor)
case `min`:
return min(valueExtractor)
return min(valueExtractorWithDate)
case `max`:
return max(valueExtractor)
return max(valueExtractorWithDate)
default:
throw new UnsupportedAggregateFunctionError(aggExpr.name)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/utils/comparison.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ascComparator = (a: any, b: any, opts: CompareOptions): number => {

// If both are dates, compare them
if (a instanceof Date && b instanceof Date) {
return a.getTime() - b.getTime()
return a.valueOf() - b.valueOf()
}

// If at least one of the values is an object, use stable IDs for comparison
Expand Down
7 changes: 7 additions & 0 deletions packages/db/tests/query/group-by.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Order = {
amount: number
status: string
date: string
date_instance: Date
product_category: string
quantity: number
discount: number
Expand All @@ -37,6 +38,7 @@ const sampleOrders: Array<Order> = [
amount: 100,
status: `completed`,
date: `2023-01-01`,
date_instance: new Date(`2023-01-01`),
product_category: `electronics`,
quantity: 2,
discount: 0,
Expand All @@ -48,6 +50,7 @@ const sampleOrders: Array<Order> = [
amount: 200,
status: `completed`,
date: `2023-01-15`,
date_instance: new Date(`2023-01-15`),
product_category: `electronics`,
quantity: 1,
discount: 10,
Expand Down Expand Up @@ -81,6 +84,8 @@ describe(`Query GROUP BY Types`, () => {
avg_amount: avg(orders.amount),
min_amount: min(orders.amount),
max_amount: max(orders.amount),
min_date: min(orders.date_instance),
max_date: max(orders.date_instance),
})),
})

Expand All @@ -93,6 +98,8 @@ describe(`Query GROUP BY Types`, () => {
avg_amount: number
min_amount: number
max_amount: number
min_date: Date
max_date: Date
}
| undefined
>()
Expand Down
Loading
Loading