From 3e2a71644695fadd78c794febc31a4a559e717e3 Mon Sep 17 00:00:00 2001 From: Yitz Willroth Date: Wed, 11 Jun 2025 10:58:15 -0400 Subject: [PATCH] :feat: add savepoint management for database transactions --- .../Database/Concerns/ManagesSavepoints.php | 412 ++++++++++++++++++ .../Database/Concerns/ManagesTransactions.php | 34 +- src/Illuminate/Database/Connection.php | 5 + .../Database/ConnectionInterface.php | 48 ++ .../Database/Events/SavepointCreated.php | 15 + .../Database/Events/SavepointReleased.php | 15 + .../Database/Events/SavepointRolledBack.php | 16 + .../Database/Query/Grammars/Grammar.php | 72 +-- .../Query/Grammars/MariaDbGrammar.php | 40 ++ .../Database/Query/Grammars/MySqlGrammar.php | 41 ++ .../Query/Grammars/PostgresGrammar.php | 41 ++ .../Database/Query/Grammars/SQLiteGrammar.php | 40 ++ .../Query/Grammars/SqlServerGrammar.php | 41 +- .../Database/SqlServerConnection.php | 20 + src/Illuminate/Support/Facades/DB.php | 8 + 15 files changed, 791 insertions(+), 57 deletions(-) create mode 100644 src/Illuminate/Database/Concerns/ManagesSavepoints.php create mode 100644 src/Illuminate/Database/Events/SavepointCreated.php create mode 100644 src/Illuminate/Database/Events/SavepointReleased.php create mode 100644 src/Illuminate/Database/Events/SavepointRolledBack.php diff --git a/src/Illuminate/Database/Concerns/ManagesSavepoints.php b/src/Illuminate/Database/Concerns/ManagesSavepoints.php new file mode 100644 index 000000000000..b3604376a9de --- /dev/null +++ b/src/Illuminate/Database/Concerns/ManagesSavepoints.php @@ -0,0 +1,412 @@ +> + */ + protected array $savepoints = []; + + /** + * @var string Prefix used for savepoint names. + */ + protected string $savepointPrefix = '__savepoint__'; + + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return $this->queryGrammar->supportsSavepoints(); + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return $this->queryGrammar->supportsSavepointRelease(); + } + + /** + * Create a savepoint within the current transaction. Optionally provide a callback + * to be executed following creation of the savepoint. If the callback fails, the transaction + * will be rolled back to the savepoint. The savepoint will be released after the callback + * has been executed. + * + * @throws Throwable + */ + public function savepoint(string $name, ?callable $callback = null): mixed + { + if (! $this->supportsSavepoints()) { + $this->savepointsUnsupportedError(); + } + + if (! $this->transactionLevel()) { + $this->savepointOutsideTransactionError(); + } + + if ($this->hasSavepoint($name)) { + $this->duplicateSavepointError($name); + } + + if ($this->getPdo()->exec($this->queryGrammar->compileSavepoint($this->wrapSavepointName($name))) === false) { + $this->savepointActionFailedError(); + } + + $this->savepoints[$this->transactionLevel()] ??= []; + + $this->savepoints[$this->transactionLevel()][] = $this->wrapSavepointName($name); + + $this->event(new SavepointCreated($this, $name)); + + if (! is_null($callback)) { + try { + return $callback(); + } catch (Throwable $e) { + if ($this->hasSavepoint($name)) { + $this->rollbackToSavepoint($name); + } + + throw $e; + } finally { + if ($this->supportsSavepointRelease() && $this->hasSavepoint($name)) { + $this->releaseSavepoint($name); + } + } + } + + return true; + } + + /** + * Rollback to a named savepoint within the current transaction. + * + * @throws Throwable + */ + public function rollbackToSavepoint(string $name): void + { + if (! $this->supportsSavepoints()) { + $this->savepointsUnsupportedError(); + } + + if (! $this->hasSavepoint($name)) { + $this->unknownSavepointError($name); + } + + $name = $this->wrapSavepointName($name); + + if ($this->getPdo()->exec($this->queryGrammar->compileRollbackToSavepoint($name)) === false) { + $this->savepointActionFailedError(); + } + + if (($position = array_search($name, $this->savepoints[$this->transactionLevel()], true)) !== false) { + $released = array_slice( + $this->savepoints[$this->transactionLevel()], + $position + 1, + count($this->savepoints[$this->transactionLevel()]) - $position, + true + ); + + $this->savepoints[$this->transactionLevel()] = array_slice( + $this->savepoints[$this->transactionLevel()], + 0, + $position + 1, + true + ); + } + + $this->event( + new SavepointRolledBack( + $this, $this->unwrapSavepointName($name), + array_map(function ($name) { + return $this->unwrapSavepointName($name); + }, $released ?? []) + ) + ); + } + + /** + * Release a savepoint from the current transaction. + * + * @throws Throwable + */ + public function releaseSavepoint(string $name, ?int $level = null): void + { + if (! $this->supportsSavepoints()) { + $this->savepointsUnsupportedError(); + } + + if (! $this->supportsSavepointRelease()) { + $this->savepointReleaseUnsupportedError(); + } + + if (! $this->hasSavepoint($name)) { + $this->unknownSavepointError($name); + } + + $name = $this->wrapSavepointName($name); + + if ($this->getPdo()->exec($this->queryGrammar->compileReleaseSavepoint($name)) === false) { + $this->savepointActionFailedError(); + } + + $this->savepoints[$level ?? $this->transactionLevel()] = + array_values(array_diff($this->savepoints[$level ?? $this->transactionLevel()], [$name])); + + $this->event(new SavepointReleased($this, $this->unwrapSavepointName($name))); + } + + /** + * Purge all savepoints from the current transaction. + * + * @throws Throwable + */ + public function purgeSavepoints(?int $level = null): void + { + if (! $this->supportsSavepoints()) { + $this->savepointsUnsupportedError(); + } + + if (! $this->supportsSavepointRelease()) { + $this->savepointPurgeUnsupportedError(); + } + + foreach ($this->savepoints[$level ?? $this->transactionLevel()] ?? [] as $name) { + $this->releaseSavepoint($this->unwrapSavepointName($name), $level); + } + } + + /** + * Determine if the connection has a savepoint within the current transaction. + */ + public function hasSavepoint(string $name): bool + { + return in_array($this->wrapSavepointName($name), $this->savepoints[$this->transactionLevel()] ?? [], true); + } + + /** + * Get the names of all savepoints within the current transaction. + * + * @throws Throwable + */ + public function getSavepoints(): array + { + if (! $this->supportsSavepoints()) { + $this->savepointsUnsupportedError(); + } + + return array_map(function ($name) { + return $this->unwrapSavepointName($name); + }, $this->savepoints[$this->transactionLevel()] ?? []); + } + + /** + * Get the name of the current savepoint. + */ + public function getCurrentSavepoint(): ?string + { + return isset($this->savepoints[$this->transactionLevel()]) + ? $this->unwrapSavepointName(end($this->savepoints[$this->transactionLevel()])) + : null; + } + + /** + * Initialize savepoint management for the connection; sets up event + * listeners to manage savepoints during transaction events. + */ + protected function initializeSavepointManagement(bool $force = false): void + { + if (! $this->supportsSavepoints()) { + return; + } + + if ($this->savepointManagementInitialized && ! $force) { + return; + } + + $this->savepoints = []; + + $this->events?->listen(function (TransactionBeginning $event) { + $this->syncTransactionBeginning(); + }); + + $this->events?->listen(function (TransactionCommitted $event) { + $this->syncTransactionCommitted(); + }); + + $this->events?->listen(function (TransactionRolledBack $event) { + $this->syncTransactionRolledBack(); + }); + + $this->savepointManagementInitialized = true; + } + + /** + * Update savepoint management to reflect the transaction beginning event. + */ + protected function syncTransactionBeginning(): void + { + $this->savepoints[$this->transactionLevel()] = []; + } + + /** + * Update savepoint management to reflect the transaction committed event. + */ + protected function syncTransactionCommitted(): void + { + $this->syncSavepoints(); + } + + /** + * Update savepoint management to reflect the transaction rolled back event. + */ + protected function syncTransactionRolledBack(): void + { + $this->syncSavepoints(); + } + + /** + * Sync savepoints after a transaction commit or rollback. + * + * @throws Throwable + */ + protected function syncSavepoints(): void + { + foreach (array_keys($this->savepoints) as $level) { + if ($level > $this->transactionLevel()) { + if ($this->supportsSavepointRelease()) { + $this->purgeSavepoints($level); + } + + unset($this->savepoints[$level]); + } + } + + if (! $this->transactionLevel()) { + $this->savepoints = []; + } + + $this->savepoints[$this->transactionLevel() ?: 0] = []; + } + + /** + * Wrap a savepoint name with the savepoint prefix. + */ + protected function wrapSavepointName(string $name): string + { + return $this->savepointPrefix.$name; + } + + /** + * Unwrap a savepoint name from the savepoint prefix. + */ + protected function unwrapSavepointName(string $name): string + { + return substr($name, strlen($this->savepointPrefix)); + } + + /** + * Throw an error indicating that savepoints are unsupported. + * + * @throws RuntimeException + */ + protected function savepointsUnsupportedError(): void + { + throw new RuntimeException( + 'This database connection does not support creating savepoints.' + ); + } + + /** + * Throw an error indicating that releasing savepoints is unsupported. + * + * @throws RuntimeException + */ + protected function savepointReleaseUnsupportedError(): void + { + throw new RuntimeException( + 'This database connection does not support releasing savepoints.' + ); + } + + /** + * Throw an error indicating that purging savepoints is unsupported. + * + * @throws RuntimeException + */ + protected function savepointPurgeUnsupportedError(): void + { + throw new RuntimeException( + 'This database connection does not support purging savepoints.' + ); + } + + /** + * Throw an error indicating that a savepoint already exists with the given name. + * + * @throws InvalidArgumentException + */ + protected function duplicateSavepointError(string $name): void + { + throw new InvalidArgumentException( + "Savepoint '{$name}' already exists." + ); + } + + /** + * Throw an error indicating that the specified savepoint does not exist. + * + * @throws InvalidArgumentException + */ + protected function unknownSavepointError(string $name): void + { + throw new InvalidArgumentException( + "Savepoint '{$name}' does not exist." + ); + } + + /** + * Throw an error indicating that a savepoint cannot be created outside a transaction. + * + * @throws LogicException + */ + protected function savepointOutsideTransactionError(): void + { + throw new LogicException( + 'Cannot create savepoint outside of transaction.' + ); + } + + /** + * Throw an error indicating that an error occurred while executing a savepoint action. + * + * @throws RuntimeException + */ + protected function savepointActionFailedError(): void + { + throw new RuntimeException( + 'An error occurred while executing savepoint action.' + ); + } +} diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 23bc60434e49..ad4d6fef919a 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -157,20 +157,6 @@ protected function createTransaction() } } - /** - * Create a save point within the database. - * - * @return void - * - * @throws \Throwable - */ - protected function createSavepoint() - { - $this->getPdo()->exec( - $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1)) - ); - } - /** * Handle an exception from a transaction beginning. * @@ -216,6 +202,22 @@ public function commit() $this->fireConnectionEvent('committed'); } + /** + * Create a save point within the database. + * + * @return void + * + * @throws \Throwable + */ + protected function createSavepoint() + { + // we do not use ManagesSavepoint::savepoint() here because this is an internally created savepoint + // used as part of nested transaction emulation and therefore not stored in the savepoints array + $this->getPdo()->exec( + $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1)) + ); + } + /** * Handle an exception encountered when committing a transaction. * @@ -298,7 +300,9 @@ protected function performRollBack($toLevel) } } elseif ($this->queryGrammar->supportsSavepoints()) { $this->getPdo()->exec( - $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1)) + // we do not use ManagesSavepoints::rollbackToSavepoint() here because this is an internally created + // savepoint used as part of nested transaction emulation and therefore not stored in the savepoints array + $this->queryGrammar->compileRollbackToSavepoint('trans'.($toLevel + 1)) ); } } diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index f0474f0fd7de..91e728033168 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -30,6 +30,7 @@ class Connection implements ConnectionInterface use DetectsConcurrencyErrors, DetectsLostConnections, Concerns\ManagesTransactions, + Concerns\ManagesSavepoints, InteractsWithTime, Macroable; @@ -228,6 +229,8 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf $this->useDefaultQueryGrammar(); $this->useDefaultPostProcessor(); + + $this->initializeSavepointManagement(); } /** @@ -1458,6 +1461,8 @@ public function setEventDispatcher(Dispatcher $events) { $this->events = $events; + $this->initializeSavepointManagement(); + return $this; } diff --git a/src/Illuminate/Database/ConnectionInterface.php b/src/Illuminate/Database/ConnectionInterface.php index 22f866b43763..f4f8a840a230 100755 --- a/src/Illuminate/Database/ConnectionInterface.php +++ b/src/Illuminate/Database/ConnectionInterface.php @@ -167,6 +167,54 @@ public function rollBack(); */ public function transactionLevel(); + /** + * Create a savepoint within the current transaction. Optionally provide a callback + * to be executed following creation of the savepoint. If the callback fails, the transaction + * will be rolled back to the savepoint. The savepoint will be released after the callback + * has been executed. + */ + public function savepoint(string $name, ?callable $callback = null): mixed; + + /** + * Release a savepoint in the database. + */ + public function releaseSavepoint(string $name, ?int $level = null): void; + + /** + * Release all savepoints in the database. + */ + public function purgeSavepoints(?int $level = null): void; + + /** + * Rollback to a savepoint in the database. + */ + public function rollbackToSavepoint(string $name): void; + + /** + * Determine if a savepoint exists in the database. + */ + public function hasSavepoint(string $name): bool; + + /** + * Get the names of all savepoints in the database. + */ + public function getSavepoints(): array; + + /** + * Get the current savepoint name. + */ + public function getCurrentSavepoint(): ?string; + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepoints(): bool; + + /** + * Determine if the connection releases savepoints. + */ + public function supportsSavepointRelease(): bool; + /** * Execute the given callback in "dry run" mode. * diff --git a/src/Illuminate/Database/Events/SavepointCreated.php b/src/Illuminate/Database/Events/SavepointCreated.php new file mode 100644 index 000000000000..34be09d04683 --- /dev/null +++ b/src/Illuminate/Database/Events/SavepointCreated.php @@ -0,0 +1,15 @@ +getValue($this); } + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return true; + } + + /** + * Compile the SQL statement to define a savepoint. + */ + public function compileSavepoint(string $name): string + { + return 'SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint rollback. + */ + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TO SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint release. + */ + public function compileReleaseSavepoint(string $name): string + { + return 'RELEASE SAVEPOINT '.$this->wrapValue($name); + } + /** * Compile the "group by" portions of the query. * @@ -1452,38 +1492,6 @@ public function compileThreadCount() return null; } - /** - * Determine if the grammar supports savepoints. - * - * @return bool - */ - public function supportsSavepoints() - { - return true; - } - - /** - * Compile the SQL statement to define a savepoint. - * - * @param string $name - * @return string - */ - public function compileSavepoint($name) - { - return 'SAVEPOINT '.$name; - } - - /** - * Compile the SQL statement to execute a savepoint rollback. - * - * @param string $name - * @return string - */ - public function compileSavepointRollBack($name) - { - return 'ROLLBACK TO SAVEPOINT '.$name; - } - /** * Wrap the given JSON selector for boolean values. * diff --git a/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php b/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php index da51125b9774..bcdaf8eca2e3 100755 --- a/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php @@ -53,4 +53,44 @@ public function useLegacyGroupLimit(Builder $query) { return false; } + + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return true; + } + + /** + * Compile the SQL statement to define a savepoint. + */ + public function compileSavepoint(string $name): string + { + return 'SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint rollback. + */ + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TO SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint release. + */ + public function compileReleaseSavepoint(string $name): string + { + return 'RELEASE SAVEPOINT '.$this->wrapValue($name); + } } diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 6c4c2c09e212..e2bc07fe4bb1 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -496,6 +497,46 @@ public function compileThreadCount() return 'select variable_value as `Value` from performance_schema.session_status where variable_name = \'threads_connected\''; } + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return true; + } + + /** + * Compile the SQL statement to define a savepoint. + */ + public function compileSavepoint(string $name): string + { + return 'SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint rollback. + */ + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TO SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint release. + */ + public function compileReleaseSavepoint(string $name): string + { + return 'RELEASE SAVEPOINT '.$this->wrapValue($name); + } + /** * Wrap a single string in keyword identifiers. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 9207fe54565f..87019bb3bebb 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -843,4 +844,44 @@ public static function cascadeOnTrucate(bool $value = true) { self::cascadeOnTruncate($value); } + + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return true; + } + + /** + * Compile the SQL statement to define a savepoint. + */ + public function compileSavepoint(string $name): string + { + return 'SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint rollback. + */ + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TO SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint release. + */ + public function compileReleaseSavepoint(string $name): string + { + return 'RELEASE SAVEPOINT '.$this->wrapValue($name); + } } diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 9fb8d8a31589..60473508e131 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -448,6 +449,45 @@ public function compileTruncate(Builder $query) 'delete from '.$this->wrapTable($query->from) => [], ]; } + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return true; + } + + /** + * Compile the SQL statement to define a savepoint. + */ + public function compileSavepoint(string $name): string + { + return 'SAVEPOINT '.$this->wrapValue($name); + } + + /** + * Compile a rollback to savepoint statement into SQL. + */ + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TO '.$this->wrapValue($name); + } + + /** + * Compile a savepoint release statement into SQL. + */ + public function compileReleaseSavepoint(string $name): string + { + return 'RELEASE SAVEPOINT '.$this->wrapValue($name); + } /** * Wrap the given JSON selector. diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index c5e91c50e1bf..4ad53ba1ec72 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -7,6 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use RuntimeException; class SqlServerGrammar extends Grammar { @@ -476,26 +477,46 @@ public function compileJoinLateral(JoinLateralClause $join, string $expression): return trim("{$type} apply {$expression}"); } + /** + * Determine if the connection supports savepoints. + */ + public function supportsSavepoints(): bool + { + return true; + } + + /** + * Determine if the connection supports releasing savepoints. + */ + public function supportsSavepointRelease(): bool + { + return false; + } + /** * Compile the SQL statement to define a savepoint. - * - * @param string $name - * @return string */ - public function compileSavepoint($name) + public function compileSavepoint(string $name): string { - return 'SAVE TRANSACTION '.$name; + return 'SAVE TRANSACTION '.$this->wrapValue($name); } /** * Compile the SQL statement to execute a savepoint rollback. - * - * @param string $name - * @return string */ - public function compileSavepointRollBack($name) + public function compileRollbackToSavepoint(string $name): string + { + return 'ROLLBACK TRANSACTION '.$this->wrapValue($name); + } + + /** + * Compile the SQL statement to execute a savepoint release. + */ + public function compileReleaseSavepoint(string $name): string { - return 'ROLLBACK TRANSACTION '.$name; + throw new RuntimeException( + 'SQL Server does not support releasing savepoints.' + ); } /** diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index 1e6fe52bfe16..f1518632da1d 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -133,6 +133,26 @@ public function getSchemaState(?Filesystem $files = null, ?callable $processFact throw new RuntimeException('Schema dumping is not supported when using SQL Server.'); } + /** + * Release a savepoint. + * + * @throws Throwable + */ + public function releaseSavepoint(string $name, ?int $level = null): void + { + $this->savepointReleaseUnsupportedError(); + } + + /** + * Purge all savepoints. + * + * @throws Throwable + */ + public function purgeSavepoints(?int $level = null): void + { + $this->savepointReleaseUnsupportedError(); + } + /** * Get the default post processor instance. * diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 90990046ed69..5394b608cb7d 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -114,6 +114,14 @@ * @method static void rollBack(int|null $toLevel = null) * @method static int transactionLevel() * @method static void afterCommit(callable $callback) + * @method static mixed savepoint(string $name, callable|null $callback = null) + * @method static void rollbackToSavepoint(string $name) + * @method static void releaseSavepoint(string $name, int|null $level = null) + * @method static void purgeSavepoints(int|null $level = null) + * @method static array getSavepoints() + * @method static string|null getCurrentSavepoint() + * @method static bool supportsSavepoints() + * @method static bool supportsSavepointRelease() * * @see \Illuminate\Database\DatabaseManager */