diff --git a/packages/db/src/query/builder/index.ts b/packages/db/src/query/builder/index.ts index c43f3ad48..9716bce4d 100644 --- a/packages/db/src/query/builder/index.ts +++ b/packages/db/src/query/builder/index.ts @@ -20,6 +20,7 @@ import { import { createRefProxy, createRefProxyWithSelected, + isRefProxy, toExpression, } from './ref-proxy.js' import type { NamespacedRow, SingleResult } from '../../types.js' @@ -475,7 +476,19 @@ export class BaseQueryBuilder { ): QueryBuilder>> { const aliases = this._getCurrentAliases() const refProxy = createRefProxy(aliases) as RefsForContext - const selectObject = callback(refProxy) + let selectObject: any = callback(refProxy) + + // When the callback returns a RefProxy directly + // (e.g. select(({ users }) => users)), treat it as if the user + // spread it (e.g. select(({ users }) => ({ ...users }))). + // This avoids a common pitfall where returning a ref directly + // produces ref/proxy metadata instead of actual row data. + if (isRefProxy(selectObject)) { + const path: Array = selectObject.__path + const sentinelKey = `__SPREAD_SENTINEL__${path.join(`.`)}__0` + selectObject = { [sentinelKey]: true } + } + const select = buildNestedSelect(selectObject) return new BaseQueryBuilder({ diff --git a/packages/db/tests/query/select-spread.test.ts b/packages/db/tests/query/select-spread.test.ts index 30665f0fe..517d6650c 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -277,4 +277,49 @@ describe(`select spreads (runtime)`, () => { const r1 = Array.from(collection.values()).find((r) => r.id === 1) as any expect(r1.meta.author).toEqual({ name: `sam`, rating: 5 }) }) + + it(`returning a ref directly (without spread) projects the full row`, async () => { + const collection = createLiveQueryCollection((q) => + q.from({ message: messagesCollection }).select(({ message }) => message), + ) + await collection.preload() + + const results = Array.from(collection.values()) + expect(results).toHaveLength(2) + expect(results).toEqual(initialMessages) + expect(collection.get(1)).toEqual(initialMessages[0]) + }) + + it(`returning a ref directly preserves nested object fields`, async () => { + const messagesNested = createMessagesWithMetaCollection() + const collection = createLiveQueryCollection((q) => + q.from({ m: messagesNested }).select(({ m }) => m), + ) + await collection.preload() + + const results = Array.from(collection.values()) + expect(results).toEqual(nestedMessages) + + const r1 = results.find((r) => r.id === 1) as MessageWithMeta + expect(r1.meta.author.name).toBe(`sam`) + expect(r1.meta.tags).toEqual([`a`, `b`]) + }) + + it(`returning a ref directly works with live updates`, async () => { + const collection = createLiveQueryCollection((q) => + q.from({ message: messagesCollection }).select(({ message }) => message), + ) + await collection.preload() + + messagesCollection.utils.begin() + messagesCollection.utils.write({ + type: `insert`, + value: { id: 3, text: `test`, user: `alex` }, + }) + messagesCollection.utils.commit() + + const results = Array.from(collection.values()) + expect(results).toHaveLength(3) + expect(collection.get(3)).toEqual({ id: 3, text: `test`, user: `alex` }) + }) })