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
28 changes: 14 additions & 14 deletions packages/db-ivm/src/operators/groupBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,19 +211,19 @@ 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>(
valueExtractor: (value: T) => V = (v) => v as unknown as V
): AggregateFunction<T, never, V> {
return {
preMap: (data: T) => valueExtractor(data),
reduce: (values: Array<[number, number]>) => {
let minValue = Number.POSITIVE_INFINITY
reduce: (values) => {
let minValue = Number.POSITIVE_INFINITY as V
for (const [value, _multiplicity] of values) {
if (value < minValue) {
if (Number(value) < Number(minValue)) {
minValue = value
}
}
return minValue === Number.POSITIVE_INFINITY ? 0 : minValue
return minValue === Number.POSITIVE_INFINITY ? (0 as V) : minValue
Copy link
Contributor

Choose a reason for hiding this comment

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

let's type let minValue: Number | V such that we don't need the typecast here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue I have here is that I need the returned type to be the type passed in. So if they pass in a Date, I want the return type to be Date as well. That's why I ended up casting as V.

I couldn't figure out what's the best way to handle this minValue and maxValue situation since if the user returns a number we can return 0 as a valid response, but what should we do if they pass a date? Do we still return a 0, because then the response type would always be number | Date which is annoying?

},
}
}
Expand All @@ -232,19 +232,19 @@ 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>(
valueExtractor: (value: T) => V = (v) => v as unknown as V
): AggregateFunction<T, never, V> {
return {
preMap: (data: T) => valueExtractor(data),
reduce: (values: Array<[number, number]>) => {
let maxValue = Number.NEGATIVE_INFINITY
reduce: (values) => {
let maxValue = Number.NEGATIVE_INFINITY as V
for (const [value, _multiplicity] of values) {
if (value > maxValue) {
if (Number(value) > Number(maxValue)) {
maxValue = value
}
}
return maxValue === Number.NEGATIVE_INFINITY ? 0 : maxValue
return maxValue === Number.NEGATIVE_INFINITY ? (0 as V) : maxValue
},
}
}
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 @@ -454,13 +454,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 @@ -472,11 +475,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 @@ -493,6 +496,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 @@ -504,6 +509,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
39 changes: 31 additions & 8 deletions packages/db/src/indexes/btree-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { BaseIndex } from "./base-index.js"
import type { BasicExpression } from "../query/ir.js"
import type { IndexOperation } from "./base-index.js"

/**
* Normalizes values for use as Map keys to ensure proper equality comparison
* For Date objects, uses timestamp. For other objects, uses JSON serialization.
*/
function normalizeMapKey(value: any): any {
if (value instanceof Date) {
return value.getTime()
}
if (typeof value === `object` && value !== null) {
// For other objects, use JSON serialization as a fallback
// This ensures objects with same content are treated as equal
return JSON.stringify(value)
}
return value
}
/**
* Options for Ordered index
*/
Expand Down Expand Up @@ -71,14 +86,17 @@ export class BTreeIndex<
)
}

// Normalize the value for Map key usage
const normalizedValue = 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.valueMap.set(normalizedValue, keySet)
this.orderedEntries.set(indexedValue, undefined)
}

Expand All @@ -101,13 +119,16 @@ export class BTreeIndex<
return
}

if (this.valueMap.has(indexedValue)) {
const keySet = this.valueMap.get(indexedValue)!
// Normalize the value for Map key usage
const normalizedValue = 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)
Expand Down Expand Up @@ -195,7 +216,8 @@ export class BTreeIndex<
* Performs an equality lookup
*/
equalityLookup(value: any): Set<TKey> {
return new Set(this.valueMap.get(value) ?? [])
const normalizedValue = normalizeMapKey(value)
return new Set(this.valueMap.get(normalizedValue) ?? [])
}

/**
Expand Down Expand Up @@ -266,7 +288,8 @@ export class BTreeIndex<
const result = new Set<TKey>()

for (const value of values) {
const keys = this.valueMap.get(value)
const normalizedValue = normalizeMapKey(value)
const keys = this.valueMap.get(normalizedValue)
if (keys) {
keys.forEach((key) => result.add(key))
}
Expand Down
20 changes: 6 additions & 14 deletions packages/db/src/query/builder/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,23 +246,15 @@ export function sum(
return new Aggregate(`sum`, [toExpression(arg)])
}

export function min(
arg:
| RefProxy<number>
| RefProxy<number | undefined>
| number
| BasicExpression<number>
): Aggregate<number> {
export function min<T>(
arg: RefProxy<T> | RefProxy<T | undefined> | T | BasicExpression<T>
): Aggregate<T> {
return new Aggregate(`min`, [toExpression(arg)])
}

export function max(
arg:
| RefProxy<number>
| RefProxy<number | undefined>
| number
| BasicExpression<number>
): Aggregate<number> {
export function max<T>(
arg: RefProxy<T> | RefProxy<T | undefined> | T | BasicExpression<T>
): Aggregate<T> {
return new Aggregate(`max`, [toExpression(arg)])
}

Expand Down
10 changes: 8 additions & 2 deletions packages/db/src/query/compiler/evaluators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,14 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
const argA = compiledArgs[0]!
const argB = compiledArgs[1]!
return (data) => {
const a = argA(data)
const b = argB(data)
let a = argA(data)
let b = argB(data)
if (a instanceof Date) {
a = a.getTime()
}
if (b instanceof Date) {
b = b.getTime()
}
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
}

// Return the appropriate aggregate function
switch (aggExpr.name.toLowerCase()) {
case `sum`:
Expand All @@ -358,9 +372,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
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