From 23660325f16bf133388631e0bee0eb86f1a54f69 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Tue, 24 Mar 2026 14:51:40 +0100 Subject: [PATCH 1/4] test(solid-db): add findOne tests for useLiveQuery Add runtime and type tests for findOne support in solid-db's useLiveQuery. These tests currently fail, reproducing #1399. Fixes #1399 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../solid-db/tests/useLiveQuery.test-d.tsx | 91 ++++++++++++ packages/solid-db/tests/useLiveQuery.test.tsx | 134 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/solid-db/tests/useLiveQuery.test-d.tsx diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx new file mode 100644 index 000000000..5142fe62c --- /dev/null +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -0,0 +1,91 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { renderHook } from '@solidjs/testing-library' +import { createCollection } from '../../db/src/collection/index' +import { mockSyncCollectionOptions } from '../../db/tests/utils' +import { + createLiveQueryCollection, + eq, +} from '../../db/src/query/index' +import { useLiveQuery } from '../src/useLiveQuery' +import type { OutputWithVirtual } from '../../db/tests/utils' +import type { SingleResult } from '../../db/src/types' + +type Person = { + id: string + name: string + age: number + email: string + isActive: boolean + team: string +} + +describe(`useLiveQuery type assertions`, () => { + it(`should type findOne query builder to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-2`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const rendered = renderHook(() => { + return useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + }) + + expectTypeOf(rendered.result()).toMatchTypeOf< + OutputWithVirtual | undefined + >() + }) + + it(`should type findOne collection using createLiveQueryCollection to return a single row`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-2`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + }) + + expectTypeOf(liveQueryCollection).toExtend() + + const rendered = renderHook(() => { + return useLiveQuery(() => liveQueryCollection) + }) + + expectTypeOf(rendered.result()).toMatchTypeOf< + OutputWithVirtual | undefined + >() + }) + + it(`should type non-findOne queries to return an array`, () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-2`, + getKey: (person: Person) => person.id, + initialData: [], + }), + ) + + const rendered = renderHook(() => { + return useLiveQuery((q) => q.from({ collection })) + }) + + expectTypeOf(rendered.result()).toMatchTypeOf< + Array> + >() + }) +}) diff --git a/packages/solid-db/tests/useLiveQuery.test.tsx b/packages/solid-db/tests/useLiveQuery.test.tsx index 2e7452693..378c2a0fc 100644 --- a/packages/solid-db/tests/useLiveQuery.test.tsx +++ b/packages/solid-db/tests/useLiveQuery.test.tsx @@ -2453,4 +2453,138 @@ describe(`Query Collections`, () => { expect(finalIds).toEqual([`1`, `2`, `3`, `4`]) }) }) + + describe(`findOne`, () => { + it(`should return a single row with query builder`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-qb`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const rendered = renderHook(() => { + return useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + ) + }) + + // Wait for collection to sync + await waitFor(() => { + expect(rendered.result.state.size).toBe(1) + }) + + expect(rendered.result.state.get(`3`)).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + expect(rendered.result()).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + }) + + it(`should return a single row with config object`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-config`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const rendered = renderHook(() => { + return useLiveQuery(() => ({ + query: (q: any) => + q + .from({ collection }) + .where(({ collection: c }: any) => eq(c.id, `3`)) + .findOne(), + })) + }) + + // Wait for collection to sync + await waitFor(() => { + expect(rendered.result.state.size).toBe(1) + }) + + expect(rendered.result.state.get(`3`)).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + expect(rendered.result()).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + }) + + it(`should return a single row with pre-created collection`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-collection`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const liveQueryCollection = createLiveQueryCollection({ + query: (q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `3`)) + .findOne(), + }) + + const rendered = renderHook(() => { + return useLiveQuery(() => liveQueryCollection) + }) + + // Wait for collection to sync + await waitFor(() => { + expect(rendered.result.state.size).toBe(1) + }) + + expect(rendered.result.state.get(`3`)).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + + expect(rendered.result()).toMatchObject({ + id: `3`, + name: `John Smith`, + }) + }) + + it(`should return undefined when findOne matches no rows`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `test-persons-findone-empty`, + getKey: (person: Person) => person.id, + initialData: initialPersons, + }), + ) + + const rendered = renderHook(() => { + return useLiveQuery((q) => + q + .from({ collection }) + .where(({ collection: c }) => eq(c.id, `nonexistent`)) + .findOne(), + ) + }) + + // Wait for collection to be ready + await waitFor(() => { + expect(rendered.result.isReady).toBe(true) + }) + + expect(rendered.result()).toBeUndefined() + }) + }) }) From 671db9e195b2980ac1d26a2166f91dfa5cdf9482 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:53:03 +0000 Subject: [PATCH 2/4] ci: apply automated fixes --- packages/solid-db/tests/useLiveQuery.test-d.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/solid-db/tests/useLiveQuery.test-d.tsx b/packages/solid-db/tests/useLiveQuery.test-d.tsx index 5142fe62c..29c5061ea 100644 --- a/packages/solid-db/tests/useLiveQuery.test-d.tsx +++ b/packages/solid-db/tests/useLiveQuery.test-d.tsx @@ -2,10 +2,7 @@ import { describe, expectTypeOf, it } from 'vitest' import { renderHook } from '@solidjs/testing-library' import { createCollection } from '../../db/src/collection/index' import { mockSyncCollectionOptions } from '../../db/tests/utils' -import { - createLiveQueryCollection, - eq, -} from '../../db/src/query/index' +import { createLiveQueryCollection, eq } from '../../db/src/query/index' import { useLiveQuery } from '../src/useLiveQuery' import type { OutputWithVirtual } from '../../db/tests/utils' import type { SingleResult } from '../../db/src/types' From 9c62889c930be8e5fa8ef0dae586992a7bdfb306 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Tue, 24 Mar 2026 14:57:18 +0100 Subject: [PATCH 3/4] fix(solid-db): support findOne in useLiveQuery Update type overloads to use InferResultType so findOne queries return T | undefined instead of Array. Add SingleResult/NonSingleResult overloads for pre-created collections. Check config.singleResult at runtime to return the first element instead of the full array. Fixes #1399 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/solid-db/src/useLiveQuery.ts | 57 +++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/solid-db/src/useLiveQuery.ts b/packages/solid-db/src/useLiveQuery.ts index 7a5ba9595..5380bae00 100644 --- a/packages/solid-db/src/useLiveQuery.ts +++ b/packages/solid-db/src/useLiveQuery.ts @@ -17,12 +17,16 @@ import type { Accessor } from 'solid-js' import type { ChangeMessage, Collection, + CollectionConfigSingleRowOption, CollectionStatus, Context, GetResult, + InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, + NonSingleResult, QueryBuilder, + SingleResult, } from '@tanstack/db' /** @@ -97,12 +101,12 @@ import type { // Overload 1: Accept query function that always returns QueryBuilder export function useLiveQuery( queryFn: (q: InitialQueryBuilder) => QueryBuilder, -): Accessor>> & { +): Accessor> & { /** * @deprecated use function result instead * query.data -> query() */ - data: Array> + data: InferResultType state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -118,12 +122,12 @@ export function useLiveQuery( queryFn: ( q: InitialQueryBuilder, ) => QueryBuilder | undefined | null, -): Accessor>> & { +): Accessor> & { /** * @deprecated use function result instead * query.data -> query() */ - data: Array> + data: InferResultType state: ReactiveMap> collection: Collection, string | number, {}> | null status: CollectionStatus | `disabled` @@ -177,12 +181,12 @@ export function useLiveQuery( // Overload 2: Accept config object export function useLiveQuery( config: Accessor>, -): Accessor>> & { +): Accessor> & { /** * @deprecated use function result instead * query.data -> query() */ - data: Array> + data: InferResultType state: ReactiveMap> collection: Collection, string | number, {}> status: CollectionStatus @@ -228,13 +232,15 @@ export function useLiveQuery( * * ) */ -// Overload 3: Accept pre-created live query collection +// Overload 3: Accept pre-created live query collection (non-single result) export function useLiveQuery< TResult extends object, TKey extends string | number, TUtils extends Record, >( - liveQueryCollection: Accessor>, + liveQueryCollection: Accessor< + Collection & NonSingleResult + >, ): Accessor> & { /** * @deprecated use function result instead @@ -251,6 +257,31 @@ export function useLiveQuery< isCleanedUp: boolean } +// Overload 3b: Accept pre-created live query collection with singleResult: true +export function useLiveQuery< + TResult extends object, + TKey extends string | number, + TUtils extends Record, +>( + liveQueryCollection: Accessor< + Collection & SingleResult + >, +): Accessor & { + /** + * @deprecated use function result instead + * query.data -> query() + */ + data: TResult | undefined + state: ReactiveMap + collection: Collection & SingleResult + status: CollectionStatus + isLoading: boolean + isReady: boolean + isIdle: boolean + isError: boolean + isCleanedUp: boolean +} + // Implementation - use function overloads to infer the actual collection type export function useLiveQuery( configOrQueryOrCollection: (queryFn?: any) => any, @@ -393,6 +424,16 @@ export function useLiveQuery( // We have to remove getters from the resource function so we wrap it function getData() { + const currentCollection = collection() + if (currentCollection) { + const config: CollectionConfigSingleRowOption = + currentCollection.config + if (config.singleResult) { + // Force resource tracking so Suspense works + getDataResource() + return data[0] + } + } return getDataResource() } From eebdc350843b1343ea7ac61a15ff15f06398f0bc Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Tue, 24 Mar 2026 15:11:16 +0100 Subject: [PATCH 4/4] chore: add changeset for solid-db findOne fix Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/fix-solid-db-findone.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/fix-solid-db-findone.md diff --git a/.changeset/fix-solid-db-findone.md b/.changeset/fix-solid-db-findone.md new file mode 100644 index 000000000..2c6bc4281 --- /dev/null +++ b/.changeset/fix-solid-db-findone.md @@ -0,0 +1,9 @@ +--- +'@tanstack/solid-db': patch +--- + +fix(solid-db): support findOne in useLiveQuery + +`useLiveQuery` with `.findOne()` returned an array instead of a single object. Updated type overloads to use `InferResultType` so findOne queries return `T | undefined`, and added a runtime `singleResult` check to return the first element instead of the full array. + +Fixes #1399