From 254ffecaeae3d2fde9576cc76cab1b79d51c5110 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:08:06 -0400 Subject: [PATCH] fix(db-sqlite): sqlite unique validation messages (#12740) Fixes https://github.com/payloadcms/payload/issues/12628 When using sqlite, the error from the db is a bit different than Postgres. This PR allows us to extract the fieldName when using sqlite for the unique constraint error. --- packages/drizzle/src/upsertRow/index.ts | 45 ++++++++++++++++++------- test/database/config.ts | 10 ++++++ test/database/int.spec.ts | 22 +++++++++++- 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/packages/drizzle/src/upsertRow/index.ts b/packages/drizzle/src/upsertRow/index.ts index 3337a7127..b03a2f00e 100644 --- a/packages/drizzle/src/upsertRow/index.ts +++ b/packages/drizzle/src/upsertRow/index.ts @@ -380,11 +380,14 @@ export const upsertRow = async | TypeWithID>( // Error Handling // ////////////////////////////////// } catch (error) { - if (error.code === '23505') { + // Unique constraint violation error + // '23505' is the code for PostgreSQL, and 'SQLITE_CONSTRAINT_UNIQUE' is for SQLite + if (error.code === '23505' || error.code === 'SQLITE_CONSTRAINT_UNIQUE') { let fieldName: null | string = null // We need to try and find the right constraint for the field but if we can't we fallback to a generic message - if (adapter.fieldConstraints?.[tableName]) { - if (adapter.fieldConstraints[tableName]?.[error.constraint]) { + if (error.code === '23505') { + // For PostgreSQL, we can try to extract the field name from the error constraint + if (adapter.fieldConstraints?.[tableName]?.[error.constraint]) { fieldName = adapter.fieldConstraints[tableName]?.[error.constraint] } else { const replacement = `${tableName}_` @@ -397,18 +400,36 @@ export const upsertRow = async | TypeWithID>( } } } - } - if (!fieldName) { - // Last case scenario we extract the key and value from the detail on the error - const detail = error.detail - const regex = /Key \(([^)]+)\)=\(([^)]+)\)/ - const match = detail.match(regex) + if (!fieldName) { + // Last case scenario we extract the key and value from the detail on the error + const detail = error.detail + const regex = /Key \(([^)]+)\)=\(([^)]+)\)/ + const match: string[] = detail.match(regex) - if (match) { - const key = match[1] + if (match && match[1]) { + const key = match[1] - fieldName = key + fieldName = key + } + } + } else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { + /** + * For SQLite, we can try to extract the field name from the error message + * The message typically looks like: + * "UNIQUE constraint failed: table_name.field_name" + */ + const regex = /UNIQUE constraint failed: ([^.]+)\.([^.]+)/ + const match: string[] = error.message.match(regex) + + if (match && match[2]) { + if (adapter.fieldConstraints[tableName]) { + fieldName = adapter.fieldConstraints[tableName][`${match[2]}_idx`] + } + + if (!fieldName) { + fieldName = match[2] + } } } diff --git a/test/database/config.ts b/test/database/config.ts index 018e02ceb..2842d1e0f 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -746,6 +746,16 @@ export default buildConfigWithDefaults({ }, ], }, + { + slug: 'unique-fields', + fields: [ + { + name: 'slugField', + type: 'text', + unique: true, + }, + ], + }, ], globals: [ { diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index 98c78074d..d02ebaae5 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -1,7 +1,7 @@ import type { MongooseAdapter } from '@payloadcms/db-mongodb' import type { PostgresAdapter } from '@payloadcms/db-postgres/types' import type { NextRESTClient } from 'helpers/NextRESTClient.js' -import type { Payload, PayloadRequest, TypeWithID } from 'payload' +import type { Payload, PayloadRequest, TypeWithID, ValidationError } from 'payload' import { migrateRelationshipsV2_V3, @@ -2646,4 +2646,24 @@ describe('database', () => { expect(docs[1].id).toBe(post_3.id) expect(docs[2].id).toBe(post_1.id) }) + + it('should throw specific unique contraint errors', async () => { + await payload.create({ + collection: 'unique-fields', + data: { + slugField: 'unique-text', + }, + }) + + try { + await payload.create({ + collection: 'unique-fields', + data: { + slugField: 'unique-text', + }, + }) + } catch (e) { + expect((e as ValidationError).message).toEqual('The following field is invalid: slugField') + } + }) })