1515
1616use CodeIgniter \Database \BaseConnection ;
1717use CodeIgniter \Database \Exceptions \DatabaseException ;
18+ use CodeIgniter \Database \Exceptions \UniqueConstraintViolationException ;
1819use CodeIgniter \Database \RawSql ;
1920use CodeIgniter \Database \TableName ;
2021use ErrorException ;
@@ -88,6 +89,12 @@ public function connect(bool $persistent = false)
8889 throw new DatabaseException ($ error );
8990 }
9091
92+ // Use verbose errors so that pg_last_error() always includes the
93+ // 5-character SQLSTATE code, enabling reliable error classification
94+ // (e.g. '23505' for unique constraint violations) without needing
95+ // to parse ambiguous English-language message strings.
96+ pg_set_error_verbosity ($ this ->connID , PGSQL_ERRORS_VERBOSE );
97+
9198 if (! empty ($ this ->schema )) {
9299 $ this ->simpleQuery ("SET search_path TO {$ this ->schema },public " );
93100 }
@@ -203,7 +210,27 @@ public function getVersion(): string
203210 protected function execute (string $ sql )
204211 {
205212 try {
206- return pg_query ($ this ->connID , $ sql );
213+ $ result = pg_query ($ this ->connID , $ sql );
214+
215+ if ($ result === false ) {
216+ $ error = $ this ->error ();
217+
218+ log_message ('error ' , (string ) $ error ['message ' ]);
219+
220+ $ exception = $ error ['code ' ] === '23505 '
221+ ? new UniqueConstraintViolationException ((string ) $ error ['message ' ], $ error ['code ' ])
222+ : new DatabaseException ((string ) $ error ['message ' ], $ error ['code ' ]);
223+
224+ if ($ this ->DBDebug ) {
225+ throw $ exception ;
226+ }
227+
228+ $ this ->lastException = $ exception ;
229+
230+ return false ;
231+ }
232+
233+ return $ result ;
207234 } catch (ErrorException $ e ) {
208235 $ trace = array_slice ($ e ->getTrace (), 2 ); // remove the call to error handler
209236
@@ -214,9 +241,19 @@ protected function execute(string $sql)
214241 'trace ' => render_backtrace ($ trace ),
215242 ]);
216243
244+ // pg_last_error() is still populated after an ErrorException,
245+ // so we can use error() here to get the reliable SQLSTATE code.
246+ $ error = $ this ->error ();
247+
248+ $ exception = $ error ['code ' ] === '23505 '
249+ ? new UniqueConstraintViolationException ((string ) $ error ['message ' ], $ error ['code ' ], $ e )
250+ : new DatabaseException ((string ) $ error ['message ' ], $ error ['code ' ], $ e );
251+
217252 if ($ this ->DBDebug ) {
218- throw new DatabaseException ( $ e -> getMessage (), $ e -> getCode (), $ e ) ;
253+ throw $ exception ;
219254 }
255+
256+ $ this ->lastException = $ exception ;
220257 }
221258
222259 return false ;
@@ -479,12 +516,33 @@ protected function _enableForeignKeyChecks()
479516 */
480517 public function error (): array
481518 {
519+ // pg_set_error_verbosity(PGSQL_ERRORS_VERBOSE) is set during connect(),
520+ // so pg_last_error() includes the SQLSTATE code in the format:
521+ // "ERROR: 23505: duplicate key value violates unique constraint ..."
522+ $ message = pg_last_error ($ this ->connID );
523+
482524 return [
483- 'code ' => '' ,
484- 'message ' => pg_last_error ( $ this -> connID ) ,
525+ 'code ' => $ this -> extractSqlState ( $ message ) ,
526+ 'message ' => $ message ,
485527 ];
486528 }
487529
530+ /**
531+ * Extracts the 5-character SQLSTATE code from a PostgreSQL error message.
532+ *
533+ * With pg_set_error_verbosity(PGSQL_ERRORS_VERBOSE) set in connect(),
534+ * pg_last_error() always returns the verbose format:
535+ * "ERROR: 23505: duplicate key value violates unique constraint ..."
536+ */
537+ private function extractSqlState (string $ message ): string
538+ {
539+ if (preg_match ('/\bERROR:\s*([0-9A-Z]{5})\s*:/ ' , $ message , $ match ) === 1 ) {
540+ return $ match [1 ];
541+ }
542+
543+ return '' ;
544+ }
545+
488546 /**
489547 * @return int|string
490548 */
0 commit comments