Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/good-papers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

Fixed bug where orderBy would fail when a collection alias had the same name as one of its schema fields. For example, .from({ email: emailCollection }).orderBy(({ email }) => email.createdAt) now works correctly even when the collection has an email field in its schema.
11 changes: 1 addition & 10 deletions packages/db/src/query/compiler/group-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,16 +417,7 @@ export function replaceAggregatesByRefs(
}

case `ref`: {
const refExpr = havingExpr
// Check if this is a direct reference to a SELECT alias
if (refExpr.path.length === 1) {
const alias = refExpr.path[0]!
if (selectClause[alias]) {
// This is a reference to a SELECT alias, convert to result.alias
return new PropRef([resultAlias, alias])
}
}
// Return as-is for other refs
// Non-aggregate refs are passed through unchanged (they reference table columns)
return havingExpr as BasicExpression
}

Expand Down
18 changes: 6 additions & 12 deletions packages/db/src/query/compiler/order-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,12 @@ export function processOrderBy(

// Create a value extractor function for the orderBy operator
const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {
// For ORDER BY expressions, we need to provide access to both:
// 1. The original namespaced row data (for direct table column references)
// 2. The __select_results (for SELECT alias references)

// Create a merged context for expression evaluation
const orderByContext = { ...row }

// If there are select results, merge them at the top level for alias access
if (row.__select_results) {
// Add select results as top-level properties for alias access
Object.assign(orderByContext, row.__select_results)
}
// The namespaced row contains:
// 1. Table aliases as top-level properties (e.g., row["tableName"])
// 2. SELECT results in __select_results (e.g., row.__select_results["aggregateAlias"])
// The replaceAggregatesByRefs function has already transformed any aggregate expressions
// that match SELECT aggregates to use the __select_results namespace.
const orderByContext = row

if (orderByClause.length > 1) {
// For multiple orderBy columns, create a composite key
Expand Down
77 changes: 77 additions & 0 deletions packages/db/tests/query/order-by.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2172,3 +2172,80 @@ describe(`Query2 OrderBy Compiler`, () => {
createOrderByTests(`off`)
createOrderByTests(`eager`)
})

describe(`OrderBy with collection alias conflicts`, () => {
type EmailSchema = {
email: string
createdAt: Date
}

const date1 = new Date(`2024-01-01`)
const date2 = new Date(`2024-01-02`)
const date3 = new Date(`2024-01-03`)

const emailCollection = createCollection<EmailSchema>({
...mockSyncCollectionOptions({
id: `emails`,
getKey: (item) => item.email,
initialData: [
{ email: `first@test.com`, createdAt: date1 },
{ email: `second@test.com`, createdAt: date2 },
{ email: `third@test.com`, createdAt: date3 },
],
}),
})

it(`should work when alias does not conflict with field name`, () => {
// This should work fine - alias "t" doesn't conflict with any field
const liveCollection = createLiveQueryCollection({
startSync: true,
query: (q) =>
q.from({ t: emailCollection }).orderBy(({ t }) => t.createdAt, `desc`),
})

const result = liveCollection.toArray

expect(result).toHaveLength(3)
expect(result[0]?.email).toBe(`third@test.com`)
expect(result[1]?.email).toBe(`second@test.com`)
expect(result[2]?.email).toBe(`first@test.com`)
})

it(`should work when alias DOES conflict with field name`, () => {
// This breaks - alias "email" conflicts with field "email"
const liveCollection = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ email: emailCollection })
.orderBy(({ email }) => email.createdAt, `desc`),
})

const result = liveCollection.toArray

expect(result).toHaveLength(3)
// The sorting should work - most recent first
expect(result[0]?.email).toBe(`third@test.com`)
expect(result[1]?.email).toBe(`second@test.com`)
expect(result[2]?.email).toBe(`first@test.com`)
})

it(`should also work for createdAt alias conflict`, () => {
// This should also work - alias "createdAt" conflicts with field "createdAt"
const liveCollection = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ createdAt: emailCollection })
.orderBy(({ createdAt }) => createdAt.email, `asc`),
})

const result = liveCollection.toArray as Array<EmailSchema>

expect(result).toHaveLength(3)
// The sorting should work - alphabetically by email
expect(result[0]?.email).toBe(`first@test.com`)
expect(result[1]?.email).toBe(`second@test.com`)
expect(result[2]?.email).toBe(`third@test.com`)
})
})
Loading