Skip to content

[master] Savepoint Support for Database Transactions #55996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
412 changes: 412 additions & 0 deletions src/Illuminate/Database/Concerns/ManagesSavepoints.php

Large diffs are not rendered by default.

34 changes: 19 additions & 15 deletions src/Illuminate/Database/Concerns/ManagesTransactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -216,6 +202,22 @@ public function commit()
$this->fireConnectionEvent('committed');
}

/**
* Create a save point within the database.
*
* @return void
*
* @throws \Throwable
*/
protected function createSavepoint()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you move this method below within MR?

Copy link
Contributor Author

@yitzwillroth yitzwillroth Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organization. The Connection uses the trait, so it's still accessible in the same place, just lives together with the other (new) savepoint methods.

{
// 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.
*
Expand Down Expand Up @@ -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))
);
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/Illuminate/Database/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Connection implements ConnectionInterface
use DetectsConcurrencyErrors,
DetectsLostConnections,
Concerns\ManagesTransactions,
Concerns\ManagesSavepoints,
InteractsWithTime,
Macroable;

Expand Down Expand Up @@ -228,6 +229,8 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf
$this->useDefaultQueryGrammar();

$this->useDefaultPostProcessor();

$this->initializeSavepointManagement();
}

/**
Expand Down Expand Up @@ -1458,6 +1461,8 @@ public function setEventDispatcher(Dispatcher $events)
{
$this->events = $events;

$this->initializeSavepointManagement();

return $this;
}

Expand Down
48 changes: 48 additions & 0 deletions src/Illuminate/Database/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
15 changes: 15 additions & 0 deletions src/Illuminate/Database/Events/SavepointCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Illuminate\Database\Events;

use Illuminate\Database\Connection;

class SavepointCreated extends ConnectionEvent
{
public function __construct(
Connection $connection,
public string $savepoint
) {
parent::__construct($connection);
}
}
15 changes: 15 additions & 0 deletions src/Illuminate/Database/Events/SavepointReleased.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Illuminate\Database\Events;

use Illuminate\Database\Connection;

class SavepointReleased extends ConnectionEvent
{
public function __construct(
Connection $connection,
public string $savepoint
) {
parent::__construct($connection);
}
}
16 changes: 16 additions & 0 deletions src/Illuminate/Database/Events/SavepointRolledBack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Illuminate\Database\Events;

use Illuminate\Database\Connection;

class SavepointRolledBack extends ConnectionEvent
{
public function __construct(
Connection $connection,
public string $savepoint,
public array $releasedSavepoints = []
) {
parent::__construct($connection);
}
}
72 changes: 40 additions & 32 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,46 @@ public function whereExpression(Builder $query, $where)
return $where['column']->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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
40 changes: 40 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
41 changes: 41 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
Loading
Loading