From 2cd5edb0a64cf9f9df8e445678e823ff9f7955d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 20:06:43 +0000 Subject: [PATCH 1/2] fix(db): resolve select() returning ref objects when not using spread operator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a RefProxy is returned directly from the select callback (e.g. `.select(({ users }) => users)`), convert it to a spread sentinel so it behaves identically to the spread form (`.select(({ users }) => ({ ...users }))`). Previously, the direct return form would pass a RefProxy through buildNestedSelect → toExpr, producing a PropRef whose own properties (type, path) were iterated by the compiler instead of the actual row data — yielding ref metadata objects instead of the real data. The fix detects RefProxy at the top level of select() and synthesizes the equivalent spread sentinel before handing off to buildNestedSelect. https://claude.ai/code/session_01KAGFqbqFxY2YYV4uTVTBa8 --- packages/db/src/query/builder/index.ts | 15 +++++- packages/db/tests/query/select-spread.test.ts | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) 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..ddd3b7738 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -277,4 +277,53 @@ 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` }) + }) }) From 1b1bf62c9d67cfd6657892fde5d1ad49b553915f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:19:15 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- packages/db/tests/query/select-spread.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/db/tests/query/select-spread.test.ts b/packages/db/tests/query/select-spread.test.ts index ddd3b7738..517d6650c 100644 --- a/packages/db/tests/query/select-spread.test.ts +++ b/packages/db/tests/query/select-spread.test.ts @@ -280,9 +280,7 @@ describe(`select spreads (runtime)`, () => { it(`returning a ref directly (without spread) projects the full row`, async () => { const collection = createLiveQueryCollection((q) => - q - .from({ message: messagesCollection }) - .select(({ message }) => message), + q.from({ message: messagesCollection }).select(({ message }) => message), ) await collection.preload() @@ -309,9 +307,7 @@ describe(`select spreads (runtime)`, () => { it(`returning a ref directly works with live updates`, async () => { const collection = createLiveQueryCollection((q) => - q - .from({ message: messagesCollection }) - .select(({ message }) => message), + q.from({ message: messagesCollection }).select(({ message }) => message), ) await collection.preload()