Skip to content

Commit 3d611b1

Browse files
authored
feat: orderBy in the query builder (#65)
1 parent 22faff9 commit 3d611b1

File tree

10 files changed

+354
-93
lines changed

10 files changed

+354
-93
lines changed

packages/optimistic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Core optimistic updates library",
44
"version": "0.0.3",
55
"dependencies": {
6-
"@electric-sql/d2ts": "^0.1.4",
6+
"@electric-sql/d2ts": "^0.1.5",
77
"@standard-schema/spec": "^1.0.0",
88
"@tanstack/store": "^0.7.0"
99
},

packages/optimistic/src/collection.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,21 @@ export class Collection<T extends object = Record<string, unknown>> {
262262
// Create a derived array from the map to avoid recalculating it
263263
this.derivedArray = new Derived({
264264
fn: ({ currDepVals: [stateMap] }) => {
265-
return Array.from(stateMap.values())
265+
// Collections returned by a query that has an orderBy are annotated
266+
// with the _orderByIndex field.
267+
// This is used to sort the array when it's derived.
268+
const array: Array<T & { _orderByIndex?: number }> = Array.from(
269+
stateMap.values()
270+
)
271+
if (array[0] && `_orderByIndex` in array[0]) {
272+
;(array as Array<T & { _orderByIndex: number }>).sort((a, b) => {
273+
if (a._orderByIndex === b._orderByIndex) {
274+
return 0
275+
}
276+
return a._orderByIndex < b._orderByIndex ? -1 : 1
277+
})
278+
}
279+
return array
266280
},
267281
deps: [this.derivedState],
268282
})

packages/optimistic/src/query/order-by.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function processOrderBy(
137137
}
138138
// if a and b are both booleans, compare them
139139
if (typeof a === `boolean` && typeof b === `boolean`) {
140-
return a ? 1 : -1
140+
return a === b ? 0 : a ? 1 : -1
141141
}
142142
// if a and b are both dates, compare them
143143
if (a instanceof Date && b instanceof Date) {
@@ -149,11 +149,34 @@ export function processOrderBy(
149149
}
150150
// if a and b are both arrays, compare them element by element
151151
if (Array.isArray(a) && Array.isArray(b)) {
152-
for (let i = 0; i < a.length; i++) {
153-
const result = comparator(a[i], b[i])
154-
if (result !== 0) return result
152+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
153+
// Get the values from the array
154+
const aVal = a[i]
155+
const bVal = b[i]
156+
157+
// Compare the values
158+
let result: number
159+
160+
if (typeof aVal === `boolean` && typeof bVal === `boolean`) {
161+
// Special handling for booleans - false comes before true
162+
result = aVal === bVal ? 0 : aVal ? 1 : -1
163+
} else if (typeof aVal === `number` && typeof bVal === `number`) {
164+
// Numeric comparison
165+
result = aVal - bVal
166+
} else if (typeof aVal === `string` && typeof bVal === `string`) {
167+
// String comparison
168+
result = aVal.localeCompare(bVal)
169+
} else {
170+
// Default comparison using the general comparator
171+
result = comparator(aVal, bVal)
172+
}
173+
174+
if (result !== 0) {
175+
return result
176+
}
155177
}
156-
return 0
178+
// All elements are equal up to the minimum length
179+
return a.length - b.length
157180
}
158181
// if a and b are both null/undefined, return 0
159182
if ((a === null || a === undefined) && (b === null || b === undefined)) {

packages/optimistic/src/query/query-builder.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,12 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
262262
return select
263263
})
264264

265+
// Ensure we have an orderByIndex in the select if we have an orderBy
266+
// This is required if select is called after orderBy
267+
if (this._query.orderBy) {
268+
validatedSelects.push({ _orderByIndex: { ORDER_INDEX: `numeric` } })
269+
}
270+
265271
const newBuilder = new BaseQueryBuilder<TContext>(
266272
(this as BaseQueryBuilder<TContext>).query
267273
)
@@ -704,6 +710,13 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
704710
// Set the orderBy clause
705711
newBuilder.query.orderBy = orderBy
706712

713+
// Ensure we have an orderByIndex in the select if we have an orderBy
714+
// This is required if select is called before orderBy
715+
newBuilder.query.select = [
716+
...(newBuilder.query.select ?? []),
717+
{ _orderByIndex: { ORDER_INDEX: `numeric` } },
718+
]
719+
707720
return newBuilder as QueryBuilder<TContext>
708721
}
709722

packages/optimistic/tests/query/query-builder/key-by.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,13 @@ describe(`QueryBuilder.keyBy`, () => {
9797
expect(builtQuery.as).toBe(`e`)
9898
expect(builtQuery.join).toBeDefined()
9999
expect(builtQuery.where).toBeDefined()
100-
expect(builtQuery.select).toHaveLength(3)
100+
expect(builtQuery.select).toHaveLength(4)
101+
expect(builtQuery.select).toEqual([
102+
`@e.id`,
103+
`@e.name`,
104+
`@d.name`,
105+
{ _orderByIndex: { ORDER_INDEX: `numeric` } }, // Added by the orderBy method
106+
])
101107
expect(builtQuery.orderBy).toBe(`@e.salary`)
102108
expect(builtQuery.limit).toBe(10)
103109
expect(builtQuery.offset).toBe(5)

packages/optimistic/tests/query/query-builder/order-by.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,12 @@ describe(`QueryBuilder orderBy, limit, and offset`, () => {
125125
expect(builtQuery.as).toBe(`e`)
126126
expect(builtQuery.join).toBeDefined()
127127
expect(builtQuery.where).toBeDefined()
128-
expect(builtQuery.select).toHaveLength(3)
128+
expect(builtQuery.select).toEqual([
129+
`@e.id`,
130+
`@e.name`,
131+
`@d.name`,
132+
{ _orderByIndex: { ORDER_INDEX: `numeric` } }, // Added by the orderBy method
133+
])
129134
})
130135
})
131136
})

0 commit comments

Comments
 (0)