1818use CodeIgniter \Database \Exceptions \RetryableTransactionException ;
1919use CodeIgniter \Database \Exceptions \UniqueConstraintViolationException ;
2020use CodeIgniter \Events \Events ;
21+ use CodeIgniter \Exceptions \InvalidArgumentException ;
2122use CodeIgniter \I18n \Time ;
2223use Exception ;
2324use ReflectionClass ;
@@ -228,6 +229,11 @@ abstract class BaseConnection implements ConnectionInterface
228229 */
229230 protected ?DatabaseException $ lastException = null ;
230231
232+ /**
233+ * The first database exception that caused the current transaction to fail.
234+ */
235+ protected ?DatabaseException $ transFailureException = null ;
236+
231237 /**
232238 * Connection ID
233239 *
@@ -860,7 +866,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s
860866 $ query ->setDuration ($ startTime , $ startTime );
861867
862868 // This will trigger a rollback if transactions are being used
863- $ this ->handleTransStatus ();
869+ $ this ->handleTransStatus ($ exception ?? $ this -> lastException );
864870
865871 if (
866872 $ this ->DBDebug
@@ -1082,44 +1088,67 @@ public function afterRollback(callable $callback): static
10821088 * @template TReturn
10831089 *
10841090 * @param callable(self): TReturn $callback
1091+ * @param positive-int $attempts
10851092 *
10861093 * @return false|TReturn
10871094 */
1088- public function transaction (callable $ callback ): mixed
1095+ public function transaction (callable $ callback, int $ attempts = 1 ): mixed
10891096 {
1097+ if ($ attempts < 1 ) {
1098+ throw new InvalidArgumentException ('Transaction attempts must be a positive integer. ' );
1099+ }
1100+
10901101 if (! $ this ->transEnabled ) {
10911102 return $ callback ($ this );
10921103 }
10931104
1094- if (! $ this ->transBegin ()) {
1095- return false ;
1096- }
1105+ $ attempts = $ this ->transDepth === 0 ? $ attempts : 1 ;
1106+
1107+ for ($ attempt = 1 ; $ attempt <= $ attempts ; $ attempt ++) {
1108+ if (! $ this ->transBegin ()) {
1109+ return false ;
1110+ }
10971111
1098- try {
1099- $ result = $ callback ($ this );
1100- } catch (Throwable $ e ) {
11011112 try {
1102- $ this ->transRollback ();
1103- } catch (Throwable $ rollbackException ) {
1104- log_message ('error ' , 'Database: Transaction callback threw an exception before rollback failed: ' . $ e );
1113+ $ result = $ callback ($ this );
1114+ } catch (Throwable $ e ) {
1115+ try {
1116+ $ this ->transRollback ();
1117+ } catch (Throwable $ rollbackException ) {
1118+ log_message ('error ' , 'Database: Transaction callback threw an exception before rollback failed: ' . $ e );
1119+
1120+ throw $ rollbackException ;
1121+ } finally {
1122+ if ($ this ->transDepth > 0 ) {
1123+ $ this ->transStatus = false ;
1124+ } elseif ($ this ->transStrict === false ) {
1125+ $ this ->transStatus = true ;
1126+ }
1127+ }
11051128
1106- throw $ rollbackException ;
1107- } finally {
1108- if ($ this ->transDepth > 0 ) {
1109- $ this ->transStatus = false ;
1110- } elseif ($ this ->transStrict === false ) {
1111- $ this ->transStatus = true ;
1129+ if ($ this ->transDepth === 0 && $ e instanceof RetryableTransactionException && $ attempt < $ attempts ) {
1130+ $ this ->prepareTransactionRetry ();
1131+
1132+ continue ;
11121133 }
1134+
1135+ throw $ e ;
11131136 }
11141137
1115- throw $ e ;
1116- }
1138+ if (! $ this ->transComplete ()) {
1139+ if ($ this ->transDepth === 0 && $ this ->transFailureException instanceof RetryableTransactionException && $ attempt < $ attempts ) {
1140+ $ this ->prepareTransactionRetry ();
11171141
1118- if (! $ this ->transComplete ()) {
1119- return false ;
1142+ continue ;
1143+ }
1144+
1145+ return false ;
1146+ }
1147+
1148+ return $ result ;
11201149 }
11211150
1122- return $ result ;
1151+ return false ;
11231152 }
11241153
11251154 /**
@@ -1145,7 +1174,8 @@ public function transBegin(bool $testMode = false): bool
11451174 // Reset the transaction failure flag.
11461175 // If the $testMode flag is set to TRUE transactions will be rolled back
11471176 // even if the queries produce a successful result.
1148- $ this ->transFailure = $ testMode ;
1177+ $ this ->transFailure = $ testMode ;
1178+ $ this ->transFailureException = null ;
11491179
11501180 if ($ this ->_transBegin ()) {
11511181 $ this ->transDepth ++;
@@ -1219,13 +1249,24 @@ public function resetTransStatus(): static
12191249 *
12201250 * @internal This method is for internal database component use only
12211251 */
1222- public function handleTransStatus (): void
1252+ public function handleTransStatus (? DatabaseException $ exception = null ): void
12231253 {
12241254 if ($ this ->transDepth !== 0 ) {
12251255 $ this ->transStatus = false ;
1256+ $ this ->transFailureException ??= $ exception ;
12261257 }
12271258 }
12281259
1260+ /**
1261+ * Reset transaction state that should not leak into a retry attempt.
1262+ */
1263+ protected function prepareTransactionRetry (): void
1264+ {
1265+ $ this ->transStatus = true ;
1266+ $ this ->transFailureException = null ;
1267+ $ this ->lastException = null ;
1268+ }
1269+
12291270 /**
12301271 * Run and clear callbacks registered for a successful transaction commit.
12311272 */
0 commit comments