From e8818194d28e260c3480ef253544d7fdcea1beec Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 19:31:54 +0000 Subject: [PATCH 1/4] fix: improve DuplicateKeySyncError message for distinct + custom getKey When using `.distinct()` with a custom `getKey`, the error message now explains that `.distinct()` deduplicates by the entire selected object, not by the key field. This helps users understand they should either: 1. Ensure SELECT only includes fields that make up the key 2. Use .groupBy() instead of .distinct() for explicit control 3. Remove the custom getKey to use default key behavior This addresses a production bug where users were getting confusing duplicate key errors when combining these features. --- packages/db/src/collection/sync.ts | 1 + packages/db/src/errors.ts | 22 ++++++++++++++++--- .../query/live/collection-config-builder.ts | 1 + packages/db/src/query/live/internal.ts | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/db/src/collection/sync.ts b/packages/db/src/collection/sync.ts index 841e76c1f..8684f6982 100644 --- a/packages/db/src/collection/sync.ts +++ b/packages/db/src/collection/sync.ts @@ -149,6 +149,7 @@ export class CollectionSyncManager< throw new DuplicateKeySyncError(key, this.id, { hasCustomGetKey: internal?.hasCustomGetKey ?? false, hasJoins: internal?.hasJoins ?? false, + hasDistinct: internal?.hasDistinct ?? false, }) } } diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index b6d8b385a..49d50d4fb 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -172,12 +172,28 @@ export class DuplicateKeySyncError extends CollectionOperationError { constructor( key: string | number, collectionId: string, - options?: { hasCustomGetKey?: boolean; hasJoins?: boolean }, + options?: { + hasCustomGetKey?: boolean + hasJoins?: boolean + hasDistinct?: boolean + }, ) { const baseMessage = `Cannot insert document with key "${key}" from sync because it already exists in the collection "${collectionId}"` - // Provide enhanced guidance when custom getKey is used with joins - if (options?.hasCustomGetKey && options.hasJoins) { + // Provide enhanced guidance when custom getKey is used with distinct + if (options?.hasCustomGetKey && options.hasDistinct) { + super( + `${baseMessage}. ` + + `This collection uses a custom getKey with .distinct(). ` + + `The .distinct() operator deduplicates by the ENTIRE selected object, but your custom getKey ` + + `extracts only a subset of fields. This causes multiple distinct rows (with different values ` + + `in non-key fields) to receive the same key. ` + + `To fix this, either: (1) ensure your SELECT only includes fields that make up your key, ` + + `(2) use .groupBy() instead of .distinct() to explicitly control which fields determine uniqueness, or ` + + `(3) remove the custom getKey to use the default key behavior.`, + ) + } else if (options?.hasCustomGetKey && options.hasJoins) { + // Provide enhanced guidance when custom getKey is used with joins super( `${baseMessage}. ` + `This collection uses a custom getKey with joined queries. ` + diff --git a/packages/db/src/query/live/collection-config-builder.ts b/packages/db/src/query/live/collection-config-builder.ts index 59efff818..5262afa6e 100644 --- a/packages/db/src/query/live/collection-config-builder.ts +++ b/packages/db/src/query/live/collection-config-builder.ts @@ -227,6 +227,7 @@ export class CollectionConfigBuilder< getBuilder: () => this, hasCustomGetKey: !!this.config.getKey, hasJoins: this.hasJoins(this.query), + hasDistinct: !!this.query.distinct, }, }, } diff --git a/packages/db/src/query/live/internal.ts b/packages/db/src/query/live/internal.ts index 7ec206f20..3c6a706f4 100644 --- a/packages/db/src/query/live/internal.ts +++ b/packages/db/src/query/live/internal.ts @@ -12,4 +12,5 @@ export type LiveQueryInternalUtils = { getBuilder: () => CollectionConfigBuilder hasCustomGetKey: boolean hasJoins: boolean + hasDistinct: boolean } From cf756a6259c2ec9bc5563112a54552892034c812 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 19:43:40 +0000 Subject: [PATCH 2/4] chore: add changeset for improved error message --- .changeset/distinct-getkey-error-message.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/distinct-getkey-error-message.md diff --git a/.changeset/distinct-getkey-error-message.md b/.changeset/distinct-getkey-error-message.md new file mode 100644 index 000000000..324f757aa --- /dev/null +++ b/.changeset/distinct-getkey-error-message.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Improve DuplicateKeySyncError message when using `.distinct()` with custom `getKey`. The error now explains that `.distinct()` deduplicates by the entire selected object, and provides actionable guidance to fix the issue. From 9b153c411a7ba32b032cdd7efd46f821ce434ed3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 20:46:59 +0000 Subject: [PATCH 3/4] fix: remove misleading groupBy suggestion from error message --- packages/db/src/errors.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 49d50d4fb..0b3bdb857 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -185,12 +185,11 @@ export class DuplicateKeySyncError extends CollectionOperationError { super( `${baseMessage}. ` + `This collection uses a custom getKey with .distinct(). ` + - `The .distinct() operator deduplicates by the ENTIRE selected object, but your custom getKey ` + - `extracts only a subset of fields. This causes multiple distinct rows (with different values ` + - `in non-key fields) to receive the same key. ` + - `To fix this, either: (1) ensure your SELECT only includes fields that make up your key, ` + - `(2) use .groupBy() instead of .distinct() to explicitly control which fields determine uniqueness, or ` + - `(3) remove the custom getKey to use the default key behavior.`, + `The .distinct() operator deduplicates by the ENTIRE selected object (standard SQL behavior), ` + + `but your custom getKey extracts only a subset of fields. This causes multiple distinct rows ` + + `(with different values in non-key fields) to receive the same key. ` + + `To fix this, either: (1) ensure your SELECT only includes fields that uniquely identify each row, or ` + + `(2) remove the custom getKey to use the default key behavior.`, ) } else if (options?.hasCustomGetKey && options.hasJoins) { // Provide enhanced guidance when custom getKey is used with joins From bb6b8c3bab30d22a39fc5598e5ded06c6a6fb04e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 20:57:00 +0000 Subject: [PATCH 4/4] fix: add back groupBy suggestion with min()/max() aggregates --- packages/db/src/errors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/db/src/errors.ts b/packages/db/src/errors.ts index 0b3bdb857..dc1c7b900 100644 --- a/packages/db/src/errors.ts +++ b/packages/db/src/errors.ts @@ -188,8 +188,9 @@ export class DuplicateKeySyncError extends CollectionOperationError { `The .distinct() operator deduplicates by the ENTIRE selected object (standard SQL behavior), ` + `but your custom getKey extracts only a subset of fields. This causes multiple distinct rows ` + `(with different values in non-key fields) to receive the same key. ` + - `To fix this, either: (1) ensure your SELECT only includes fields that uniquely identify each row, or ` + - `(2) remove the custom getKey to use the default key behavior.`, + `To fix this, either: (1) ensure your SELECT only includes fields that uniquely identify each row, ` + + `(2) use .groupBy() with min()/max() aggregates to select one value per group, or ` + + `(3) remove the custom getKey to use the default key behavior.`, ) } else if (options?.hasCustomGetKey && options.hasJoins) { // Provide enhanced guidance when custom getKey is used with joins