diff --git a/.changeset/fix-nullable-join-select-ref-typing.md b/.changeset/fix-nullable-join-select-ref-typing.md new file mode 100644 index 000000000..0c6a09e0e --- /dev/null +++ b/.changeset/fix-nullable-join-select-ref-typing.md @@ -0,0 +1,7 @@ +--- +'@tanstack/db': patch +--- + +fix(db): use `Ref` brand instead of `Ref | undefined` for nullable join refs in declarative select + +The declarative `select()` callback receives proxy objects that record property accesses. These proxies are always truthy at build time, but nullable join sides (left/right/full) were typed as `Ref | undefined`, misleading users into using `?.` and `??` operators that have no effect at runtime. Nullable join refs are now typed as `Ref`, which allows direct property access without optional chaining while correctly producing `T | undefined` in the result type. diff --git a/packages/db-collection-e2e/src/suites/joins.suite.ts b/packages/db-collection-e2e/src/suites/joins.suite.ts index 5f25bb20b..2a953d38c 100644 --- a/packages/db-collection-e2e/src/suites/joins.suite.ts +++ b/packages/db-collection-e2e/src/suites/joins.suite.ts @@ -24,9 +24,9 @@ export function createJoinsTestSuite(getConfig: () => Promise) { eq(user.id, post.userId), ) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, + postTitle: post.title, })), ) @@ -53,12 +53,12 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId), ) - .where(({ post }) => gt(post!.viewCount, 10)) + .where(({ post }) => gt(post.viewCount, 10)) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, - viewCount: post!.viewCount, + postTitle: post.title, + viewCount: post.viewCount, })), ) @@ -88,9 +88,9 @@ export function createJoinsTestSuite(getConfig: () => Promise) { eq(user.id, post.userId), ) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, + postTitle: post.title, })), ) @@ -117,12 +117,12 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId), ) - .orderBy(({ post }) => post!.viewCount, `desc`) + .orderBy(({ post }) => post.viewCount, `desc`) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, - viewCount: post!.viewCount, + postTitle: post.title, + viewCount: post.viewCount, })), ) @@ -149,7 +149,7 @@ export function createJoinsTestSuite(getConfig: () => Promise) { for (let i = 1; i < results.length; i++) { const prevCount = results[i - 1]!.viewCount const currCount = results[i]!.viewCount - expect(prevCount).toBeGreaterThanOrEqual(currCount) + expect(prevCount!).toBeGreaterThanOrEqual(currCount!) } await query.cleanup() @@ -166,13 +166,13 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .join({ post: postsCollection }, ({ user, post }) => eq(user.id, post.userId), ) - .orderBy(({ post }) => post!.id, `asc`) + .orderBy(({ post }) => post.id, `asc`) .limit(10) .offset(5) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, + postTitle: post.title, })), ) @@ -194,13 +194,13 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .from({ user: users }) .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) .join({ comment: comments }, ({ post, comment }) => - eq(post!.id, comment.postId), + eq(post.id, comment.postId), ) .select(({ user, post, comment }) => ({ - id: comment!.id, + id: comment.id, userName: user.name, - postTitle: post!.title, - commentText: comment!.text, + postTitle: post.title, + commentText: comment.text, })), ) @@ -225,16 +225,16 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .from({ user: users }) .where(({ user }) => eq(user.isActive, true)) .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) - .where(({ post }) => isNull(post!.deletedAt)) + .where(({ post }) => isNull(post.deletedAt)) .join({ comment: comments }, ({ post, comment }) => - eq(post!.id, comment.postId), + eq(post.id, comment.postId), ) - .where(({ comment }) => isNull(comment!.deletedAt)) + .where(({ comment }) => isNull(comment.deletedAt)) .select(({ user, post, comment }) => ({ - id: comment!.id, + id: comment.id, userName: user.name, - postTitle: post!.title, - commentText: comment!.text, + postTitle: post.title, + commentText: comment.text, })), ) @@ -264,13 +264,13 @@ export function createJoinsTestSuite(getConfig: () => Promise) { eq(user.id, post.userId), ) .join({ comment: commentsOnDemand }, ({ post, comment }) => - eq(post!.id, comment.postId), + eq(post.id, comment.postId), ) .select(({ user, post, comment }) => ({ - id: comment!.id, + id: comment.id, userName: user.name, - postTitle: post!.title, - commentText: comment!.text, + postTitle: post.title, + commentText: comment.text, })), ) @@ -296,12 +296,12 @@ export function createJoinsTestSuite(getConfig: () => Promise) { q .from({ user: users }) .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) - .where(({ post }) => gt(post!.viewCount, 50)) + .where(({ post }) => gt(post.viewCount, 50)) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, - postTitle: post!.title, - viewCount: post!.viewCount, + postTitle: post.title, + viewCount: post.viewCount, })), ) @@ -326,10 +326,10 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .where(({ user }) => gt(user.age, 30)) .join({ post: posts }, ({ user, post }) => eq(user.id, post.userId)) .select(({ user, post }) => ({ - id: post!.id, + id: post.id, userName: user.name, userAge: user.age, - postTitle: post!.title, + postTitle: post.title, })), ) @@ -359,7 +359,7 @@ export function createJoinsTestSuite(getConfig: () => Promise) { .select(({ user, post }) => ({ id: user.id, userName: user.name, - postTitle: post!.title, // May be null for users without posts + postTitle: post.title, // May be null for users without posts })), ) diff --git a/packages/db/src/query/builder/types.ts b/packages/db/src/query/builder/types.ts index 11360dd82..6dce531f8 100644 --- a/packages/db/src/query/builder/types.ts +++ b/packages/db/src/query/builder/types.ts @@ -227,18 +227,28 @@ export type ResultTypeFromSelect = WithoutRefBrand< Prettify<{ [K in keyof TSelectObject]: NeedsExtraction extends true ? ExtractExpressionType - : TSelectObject[K] extends Ref + : // Ref (full object ref or spread with RefBrand) - recursively process properties + TSelectObject[K] extends Ref ? ExtractRef - : TSelectObject[K] extends RefLeaf - ? T - : TSelectObject[K] extends RefLeaf | undefined + : // RefLeaf (simple property ref like user.name) + TSelectObject[K] extends RefLeaf + ? IsNullableRef extends true ? T | undefined - : TSelectObject[K] extends RefLeaf | null - ? T | null - : TSelectObject[K] extends Ref | undefined - ? ExtractRef | undefined - : TSelectObject[K] extends Ref | null - ? ExtractRef | null + : T + : // RefLeaf | undefined (schema-optional field) + TSelectObject[K] extends RefLeaf | undefined + ? T | undefined + : // RefLeaf | null (schema-nullable field) + TSelectObject[K] extends RefLeaf | null + ? IsNullableRef> extends true + ? T | null | undefined + : T | null + : // Ref | undefined (optional object-type schema field) + TSelectObject[K] extends Ref | undefined + ? ExtractRef> | undefined + : // Ref | null (nullable object-type schema field) + TSelectObject[K] extends Ref | null + ? ExtractRef> | null : TSelectObject[K] extends Aggregate ? T : TSelectObject[K] extends @@ -366,24 +376,17 @@ export type FunctionalHavingRow = TContext[`schema`] & (TContext[`result`] extends object ? { $selected: TContext[`result`] } : {}) /** - * RefProxyForContext - Creates ref proxies for all tables/collections in a query context + * RefsForContext - Creates ref proxies for all tables/collections in a query context * * This is the main entry point for creating ref objects in query builder callbacks. - * It handles optionality by placing undefined/null OUTSIDE the RefProxy to enable - * JavaScript's optional chaining operator (?.): + * For nullable join sides (left/right/full joins), it produces `Ref` instead + * of `Ref | undefined`. This accurately reflects that the proxy object is always + * present at build time (it's a truthy proxy that records property access paths), + * while the `Nullable` flag ensures the result type correctly includes `| undefined`. * * Examples: - * - Required field: `RefProxy` → user.name works - * - Optional field: `RefProxy | undefined` → user?.name works - * - Nullable field: `RefProxy | null` → user?.name works - * - Both optional and nullable: `RefProxy | undefined` → user?.name works - * - * The key insight is that `RefProxy` would NOT allow `user?.name` - * because the undefined is "inside" the proxy, but `RefProxy | undefined` - * does allow it because the undefined is "outside" the proxy. - * - * The logic prioritizes optional chaining by always placing `undefined` outside when - * a type is both optional and nullable (e.g., `string | null | undefined`). + * - Required field: `Ref` → user.name works, result is T + * - Nullable join side: `Ref` → user.name works, result is T | undefined * * After `select()` is called, this type also includes `$selected` which provides access * to the SELECT result fields via `$selected.fieldName` syntax. @@ -394,17 +397,17 @@ export type RefsForContext = { > extends true ? IsNonExactNullable extends true ? // T is both non-exact optional and non-exact nullable (e.g., string | null | undefined) - // Extract the non-undefined and non-null part and place undefined outside - Ref> | undefined + // Extract the non-undefined and non-null part, mark as nullable ref + Ref, true> : // T is optional (T | undefined) but not exactly undefined, and not nullable - // Extract the non-undefined part and place undefined outside - Ref> | undefined + // Extract the non-undefined part, mark as nullable ref + Ref, true> : IsNonExactNullable extends true ? // T is nullable (T | null) but not exactly null, and not optional - // Extract the non-null part and place null outside - Ref> | null + // Extract the non-null part, mark as nullable ref + Ref, true> : // T is exactly undefined, exactly null, or neither optional nor nullable - // Wrap in RefProxy as-is (includes exact undefined, exact null, and normal types) + // Wrap in Ref as-is (includes exact undefined, exact null, and normal types) Ref } & (TContext[`result`] extends object ? { $selected: Ref } @@ -479,41 +482,44 @@ type NonNull = T extends null ? never : T * It provides a recursive interface that allows nested property access while * preserving optionality and nullability correctly. * - * When spread in select clauses, it correctly produces the underlying data type - * without Ref wrappers, enabling clean spread operations. + * The `Nullable` parameter indicates whether this ref comes from a nullable + * join side (left/right/full). When `true`, the `Nullable` flag propagates + * through all nested property accesses, ensuring the result type includes + * `| undefined` for all fields accessed through this ref. * * Example usage: * ```typescript - * // Clean interface - no internal properties visible - * const users: Ref<{ id: number; profile?: { bio: string } }> = { ... } - * users.id // Ref - clean display - * users.profile?.bio // Ref - nested optional access works + * // Non-nullable ref (inner join or from table): + * select(({ user }) => ({ name: user.name })) // result: string + * + * // Nullable ref (left join right side): + * select(({ dept }) => ({ name: dept.name })) // result: string | undefined * * // Spread operations work cleanly: * select(({ user }) => ({ ...user })) // Returns User type, not Ref types * ``` */ -export type Ref = { +export type Ref = { [K in keyof T]: IsNonExactOptional extends true ? IsNonExactNullable extends true ? // Both optional and nullable IsPlainObject> extends true - ? Ref> | undefined - : RefLeaf> | undefined + ? Ref, Nullable> | undefined + : RefLeaf, Nullable> | undefined : // Optional only IsPlainObject> extends true - ? Ref> | undefined - : RefLeaf> | undefined + ? Ref, Nullable> | undefined + : RefLeaf, Nullable> | undefined : IsNonExactNullable extends true ? // Nullable only IsPlainObject> extends true - ? Ref> | null - : RefLeaf> | null + ? Ref, Nullable> | null + : RefLeaf, Nullable> | null : // Required IsPlainObject extends true - ? Ref - : RefLeaf -} & RefLeaf + ? Ref + : RefLeaf +} & RefLeaf /** * Ref - The user-facing ref type with clean IDE display @@ -527,11 +533,19 @@ export type Ref = { * - No internal properties like __refProxy, __path, __type are visible */ declare const RefBrand: unique symbol -export type RefLeaf = { readonly [RefBrand]?: T } +declare const NullableBrand: unique symbol +export type RefLeaf = { + readonly [RefBrand]?: T +} & ([Nullable] extends [true] ? { readonly [NullableBrand]?: true } : {}) + +// Detect NullableBrand by checking for the key's presence +type IsNullableRef = typeof NullableBrand extends keyof T ? true : false -// Helper type to remove RefBrand from objects +// Helper type to remove RefBrand and NullableBrand from objects type WithoutRefBrand = - T extends Record ? Omit : T + T extends Record + ? Omit + : T /** * PreserveSingleResultFlag - Conditionally includes the singleResult flag diff --git a/packages/db/tests/query/builder/callback-types.test-d.ts b/packages/db/tests/query/builder/callback-types.test-d.ts index 0e901b7fb..52060e983 100644 --- a/packages/db/tests/query/builder/callback-types.test-d.ts +++ b/packages/db/tests/query/builder/callback-types.test-d.ts @@ -122,17 +122,15 @@ describe(`Query Builder Callback Types`, () => { expectTypeOf( user.department_id, ).toEqualTypeOf | null>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.name).toEqualTypeOf | undefined>() - expectTypeOf(dept?.budget).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.name).toEqualTypeOf>() + expectTypeOf(dept.budget).toEqualTypeOf>() return { user_name: user.name, - dept_name: dept?.name, + dept_name: dept.name, user_email: user.email, - dept_budget: dept?.budget, + dept_budget: dept.budget, } }) }) @@ -263,17 +261,13 @@ describe(`Query Builder Callback Types`, () => { ) .where(({ user, dept }) => { expectTypeOf(user.active).toEqualTypeOf>() - expectTypeOf(dept?.active).toEqualTypeOf< - RefLeaf | undefined - >() - expectTypeOf(dept?.budget).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.active).toEqualTypeOf>() + expectTypeOf(dept.budget).toEqualTypeOf>() return and( eq(user.active, true), - eq(dept?.active, true), - gt(dept?.budget, 100000), + eq(dept.active, true), + gt(dept.budget, 100000), ) }) }) @@ -315,13 +309,13 @@ describe(`Query Builder Callback Types`, () => { ) .join({ project: projectsCollection }, ({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() + expectTypeOf(dept.id).toEqualTypeOf>() expectTypeOf(project.user_id).toEqualTypeOf>() expectTypeOf(project.department_id).toEqualTypeOf>() return and( eq(project.user_id, user.id), - eq(project.department_id, dept?.id), + eq(project.department_id, dept.id), ) }) }) @@ -360,10 +354,10 @@ describe(`Query Builder Callback Types`, () => { ) .orderBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.name).toEqualTypeOf | undefined>() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.name).toEqualTypeOf>() - return dept?.name + return dept.name }) }) }) @@ -400,12 +394,10 @@ describe(`Query Builder Callback Types`, () => { ) .groupBy(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() - return dept?.location + return dept.location }) }) }) @@ -481,13 +473,11 @@ describe(`Query Builder Callback Types`, () => { .join({ dept: departmentsCollection }, ({ user, dept }) => eq(user.department_id, dept.id), ) - .groupBy(({ dept }) => dept?.location) + .groupBy(({ dept }) => dept.location) .having(({ user, dept }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() return and(gt(count(user.id), 3), gt(avg(user.salary), 70000)) }) @@ -506,67 +496,51 @@ describe(`Query Builder Callback Types`, () => { }) .join({ project: projectsCollection }, ({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() expectTypeOf(project.user_id).toEqualTypeOf>() expectTypeOf(project.department_id).toEqualTypeOf>() return eq(project.user_id, user.id) }) .where(({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() - expectTypeOf(project?.user_id).toEqualTypeOf< - RefLeaf | undefined - >() - expectTypeOf(project?.department_id).toEqualTypeOf< - RefLeaf | undefined + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + expectTypeOf(project.user_id).toEqualTypeOf>() + expectTypeOf(project.department_id).toEqualTypeOf< + RefLeaf >() return and( eq(user.active, true), - eq(dept?.active, true), - eq(project?.status, `active`), + eq(dept.active, true), + eq(project.status, `active`), ) }) .groupBy(({ dept }) => { - expectTypeOf(dept?.id).toEqualTypeOf | undefined>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() - return dept?.location + expectTypeOf(dept.id).toEqualTypeOf>() + expectTypeOf(dept.location).toEqualTypeOf>() + return dept.location }) .having(({ user, project }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(project?.budget).toEqualTypeOf< - RefLeaf | undefined - >() - return and(gt(count(user.id), 2), gt(avg(project?.budget), 50000)) + expectTypeOf(project.budget).toEqualTypeOf>() + return and(gt(count(user.id), 2), gt(avg(project.budget), 50000)) }) .select(({ user, dept, project }) => { expectTypeOf(user.id).toEqualTypeOf>() - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() - expectTypeOf(project?.budget).toEqualTypeOf< - RefLeaf | undefined - >() + expectTypeOf(dept.location).toEqualTypeOf>() + expectTypeOf(project.budget).toEqualTypeOf>() return { - location: dept?.location, + location: dept.location, user_count: count(user.id), avg_salary: avg(user.salary), - total_project_budget: sum(project?.budget), - avg_project_budget: avg(project?.budget), + total_project_budget: sum(project.budget), + avg_project_budget: avg(project.budget), } }) .orderBy(({ dept }) => { - expectTypeOf(dept?.location).toEqualTypeOf< - RefLeaf | undefined - >() - return dept?.location + expectTypeOf(dept.location).toEqualTypeOf>() + return dept.location }) }) }) diff --git a/packages/db/tests/query/join.test-d.ts b/packages/db/tests/query/join.test-d.ts index 9fc1a6bde..01ca6151c 100644 --- a/packages/db/tests/query/join.test-d.ts +++ b/packages/db/tests/query/join.test-d.ts @@ -701,6 +701,127 @@ describe(`Join Alias Methods - Type Safety`, () => { }) }) +describe(`Declarative select refs should not use union with undefined for nullable joins`, () => { + test(`left-joined ref in declarative select should allow direct property access without optional chaining`, () => { + const usersCollection = createUsersCollection() + const departmentsCollection = createDepartmentsCollection() + + const query = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .leftJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ) + .select(({ user, dept }) => ({ + userName: user.name, + // dept is a proxy ref that is always present at build time, + // so direct property access should work without optional chaining + deptName: dept.name, + deptBudget: dept.budget, + })), + }) + + const results = query.toArray + + // Result fields from left-joined tables should still produce T | undefined + // because the actual data may have no matching row + expectTypeOf(results).toEqualTypeOf< + Array<{ + userName: string + deptName: string | undefined + deptBudget: number | undefined + }> + >() + }) + + test(`right-joined ref in declarative select should allow direct property access on nullable left table`, () => { + const usersCollection = createUsersCollection() + const departmentsCollection = createDepartmentsCollection() + + const query = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .rightJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ) + .select(({ user, dept }) => ({ + // user is the nullable side in a right join + userName: user.name, + deptName: dept.name, + })), + }) + + const results = query.toArray + + expectTypeOf(results).toEqualTypeOf< + Array<{ + userName: string | undefined + deptName: string + }> + >() + }) + + test(`full-joined refs in declarative select should allow direct property access on both nullable tables`, () => { + const usersCollection = createUsersCollection() + const departmentsCollection = createDepartmentsCollection() + + const query = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .fullJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ) + .select(({ user, dept }) => ({ + userName: user.name, + deptName: dept.name, + })), + }) + + const results = query.toArray + + // Both sides are nullable in a full join + expectTypeOf(results).toEqualTypeOf< + Array<{ + userName: string | undefined + deptName: string | undefined + }> + >() + }) + + test(`inner-joined ref in declarative select should allow direct property access with non-optional result`, () => { + const usersCollection = createUsersCollection() + const departmentsCollection = createDepartmentsCollection() + + const query = createLiveQueryCollection({ + query: (q) => + q + .from({ user: usersCollection }) + .innerJoin({ dept: departmentsCollection }, ({ user, dept }) => + eq(user.department_id, dept.id), + ) + .select(({ user, dept }) => ({ + userName: user.name, + deptName: dept.name, + deptBudget: dept.budget, + })), + }) + + const results = query.toArray + + // Inner join fields should never be undefined + expectTypeOf(results).toEqualTypeOf< + Array<{ + userName: string + deptName: string + deptBudget: number + }> + >() + }) +}) + describe(`Join with ArkType Schemas`, () => { test(`join with optional foreign key using ArkType schema should work`, () => { // Define ArkType schemas with optional foreign key diff --git a/packages/db/tests/query/live-query-collection.test.ts b/packages/db/tests/query/live-query-collection.test.ts index 3c73efa93..5005c0e1a 100644 --- a/packages/db/tests/query/live-query-collection.test.ts +++ b/packages/db/tests/query/live-query-collection.test.ts @@ -3,6 +3,7 @@ import { Temporal } from 'temporal-polyfill' import { createCollection } from '../../src/collection/index.js' import { and, + coalesce, createLiveQueryCollection, eq, ilike, @@ -2284,7 +2285,7 @@ describe(`createLiveQueryCollection`, () => { .select(({ base: b, related: r }) => ({ id: b.id, name: b.name, - value: r?.value, + value: r.value, })), getKey: (item) => item.id, // Valid for 1:1 joins with unique keys }) @@ -2324,9 +2325,9 @@ describe(`createLiveQueryCollection`, () => { .join({ users }, ({ comments: c, users: u }) => eq(c.userId, u.id)) .select(({ comments: c, users: u }) => ({ id: c.id, - userId: u?.id ?? c.userId, + userId: coalesce(u.id, c.userId), text: c.text, - userName: u?.name, + userName: u.name, })), getKey: (item) => item.userId, startSync: true,