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. 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..dc1c7b900 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 (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, ` + + `(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 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 }