Skip to content
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

feat(database): add support for nested transactions using savepoints #8833

Open
wants to merge 16 commits into
base: 4.6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
110 changes: 74 additions & 36 deletions system/Database/BaseConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* @property-read string $DBDriver
* @property-read string $DBPrefix
* @property-read string $DSN
* @property-read bool $enableSavepoints
* @property-read array|bool $encrypt
* @property-read array $failover
* @property-read string $hostname
Expand Down Expand Up @@ -364,6 +365,13 @@ abstract class BaseConnection implements ConnectionInterface
'time' => 'H:i:s',
];

/**
* Whether to use savepoints to fully support nesting transactions and partial rollbacks.
*
* @var bool
*/
protected $enableSavepoints = false;

/**
* Saves our connection settings.
*/
Expand Down Expand Up @@ -777,6 +785,18 @@ public function transException(bool $transExcetion)
return $this;
}

/**
* If set to true, enables the use of nested transactions
* If set to false (default), only the outermost transaction
* actually gets started and ended or rolled back.
*/
public function transSavepoints(bool $enable): self
{
$this->enableSavepoints = $enable;

return $this;
}

/**
* Complete Transaction
*/
Expand All @@ -790,13 +810,6 @@ public function transComplete(): bool
if ($this->transStatus === false || $this->transFailure === true) {
$this->transRollback();

// If we are NOT running in strict mode, we will reset
// the _trans_status flag so that subsequent groups of
// transactions will be permitted.
if ($this->transStrict === false) {
$this->transStatus = true;
}

return false;
}

Expand All @@ -820,8 +833,9 @@ public function transBegin(bool $testMode = false): bool
return false;
}

// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->transDepth > 0) {
// When transactions are nested and nested transactions are not enabled
// we only begin/commit/rollback the outermost ones
if (! $this->enableSavepoints && $this->transDepth > 0) {
$this->transDepth++;

return true;
Expand All @@ -836,51 +850,60 @@ public function transBegin(bool $testMode = false): bool
// even if the queries produce a successful result.
$this->transFailure = ($testMode === true);

if ($this->_transBegin()) {
$this->transDepth++;
$started = false;

return true;
try {
return $started = ($this->transDepth < 1 ? $this->_transBegin() : $this->_transBeginNested());
} finally {
if ($started) {
$this->transDepth++;
}
}

return false;
}

/**
* Commit Transaction
*/
public function transCommit(): bool
{
if (! $this->transEnabled || $this->transDepth === 0) {
return false;
}

// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->transDepth > 1 || $this->_transCommit()) {
$this->transDepth--;

return true;
}

return false;
return $this->_transEnd(true);
}

/**
* Rollback Transaction
*/
public function transRollback(): bool
{
if (! $this->transEnabled || $this->transDepth === 0) {
return false;
}
return $this->_transEnd(false);
}

// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->transDepth > 1 || $this->_transRollback()) {
$this->transDepth--;
private function _transEnd(bool $commit = false): bool
{
$ended = false;
$transEndMethod = $commit ? '_transCommit' : '_transRollback';
$transEndNestedMethod = $transEndMethod . 'Nested';

return true;
try {
// if nested transactions are disabled and transactions are nested we only begin/commit/rollback the outermost ones
return $ended = match (true) {
! $this->transEnabled => false,
$this->transDepth === 1 => $this->{$transEndMethod}(),
$this->enableSavepoints && $this->transDepth > 1 => $this->{$transEndNestedMethod}(),
$this->transDepth > 1 => true,
default => false,
};
} finally {
if ($ended) {
$this->transDepth = max(0, $this->transDepth - 1);

// If we are NOT running in strict mode, we will reset
// the _trans_status flag so that subsequent groups of
// transactions will be permitted.
if ($this->transStrict === false) {
$this->transStatus = true;
}
}
}

return false;
}

/**
Expand All @@ -898,6 +921,21 @@ abstract protected function _transCommit(): bool;
*/
abstract protected function _transRollback(): bool;

/**
* Begin Transaction
*/
abstract protected function _transBeginNested(): bool;

/**
* Commit Transaction
*/
abstract protected function _transCommitNested(): bool;

/**
* Rollback Transaction
*/
abstract protected function _transRollbackNested(): bool;

/**
* Returns a non-shared new instance of the query builder for this connection.
*
Expand Down Expand Up @@ -1720,7 +1758,7 @@ public function resetDataCache()
*/
public function isWriteType($sql): bool
{
return (bool) preg_match('/^\s*(WITH\s.+(\s|[)]))?"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s(?!.*\sRETURNING\s)/is', $sql);
return (bool) preg_match('/^\s*(WITH\s.+(\s|[)]))?"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE|((RELEASE|ROLLBACK TO)?\s*SAVEPOINT))\s(?!.*\sRETURNING\s)/is', $sql);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions system/Database/MySQLi/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\SavepointsForNestedTransactions;
use LogicException;
use mysqli;
use mysqli_result;
Expand All @@ -29,6 +30,10 @@
*/
class Connection extends BaseConnection
{
use SavepointsForNestedTransactions {
_savepointQueryDefault as _savePointQuery;
}

/**
* Database driver
*
Expand Down
10 changes: 10 additions & 0 deletions system/Database/OCI8/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Query;
use CodeIgniter\Database\SavepointsForNestedTransactions;
use ErrorException;
use stdClass;

Expand All @@ -26,6 +27,10 @@
*/
class Connection extends BaseConnection
{
use SavepointsForNestedTransactions {
_savepointQueryDefault as _savePointQuery;
}

/**
* Database driver
*
Expand Down Expand Up @@ -113,6 +118,11 @@
return false;
}

public function initialize()
{
return parent::initialize(); // TODO: Change the autogenerated stub

Check failure on line 123 in system/Database/OCI8/Connection.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Method CodeIgniter\Database\OCI8\Connection::initialize() with return type void returns null but should not return anything.

Check failure on line 123 in system/Database/OCI8/Connection.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Result of method CodeIgniter\Database\BaseConnection<resource,resource>::initialize() (void) is used.
}

/**
* Connect to the database.
*
Expand Down
5 changes: 5 additions & 0 deletions system/Database/Postgre/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
use CodeIgniter\Database\SavepointsForNestedTransactions;
use ErrorException;
use PgSql\Connection as PgSqlConnection;
use PgSql\Result as PgSqlResult;
Expand All @@ -29,6 +30,10 @@
*/
class Connection extends BaseConnection
{
use SavepointsForNestedTransactions {
_savepointQueryDefault as _savepointQuery;
}

/**
* Database driver
*
Expand Down
8 changes: 8 additions & 0 deletions system/Database/SQLSRV/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\SavepointsForNestedTransactions;
use stdClass;

/**
Expand All @@ -24,6 +25,8 @@
*/
class Connection extends BaseConnection
{
use SavepointsForNestedTransactions;

/**
* Database driver
*
Expand Down Expand Up @@ -564,4 +567,9 @@

return parent::isWriteType($sql);
}

protected function _savepointQuery($create, $commit): string

Check failure on line 571 in system/Database/SQLSRV/Connection.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Method CodeIgniter\Database\SQLSRV\Connection::_savepointQuery() has parameter $commit with no type specified.

Check failure on line 571 in system/Database/SQLSRV/Connection.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Method CodeIgniter\Database\SQLSRV\Connection::_savepointQuery() has parameter $create with no type specified.
{
return $create ? 'SAVE' : ($commit ? 'COMMIT' : 'ROLLBACK') . ' TRANSACTION';
}
}
5 changes: 5 additions & 0 deletions system/Database/SQLite3/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\SavepointsForNestedTransactions;
use Exception;
use SQLite3;
use SQLite3Result;
Expand All @@ -27,6 +28,10 @@
*/
class Connection extends BaseConnection
{
use SavepointsForNestedTransactions {
_savepointQueryDefault as _savepointQuery;
}

/**
* Database driver
*
Expand Down
59 changes: 59 additions & 0 deletions system/Database/SavepointsForNestedTransactions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Database;

trait SavepointsForNestedTransactions
{
private function _savepointQueryDefault(bool $create, bool $commit): string
{
return ($create ? '' : ($commit ? 'RELEASE' : 'ROLLBACK TO') . ' ') . 'SAVEPOINT';
}

abstract private function _savepointQuery(bool $begin, bool $commit): string;

/**
* Generates the SQL for managing savepoints, which
* are used to support nested transactions with SQLite3
*/
private function _savepoint(int $savepoint, bool $create, bool $commit): string
{
$savepointIdentifier = $this->escapeIdentifier('__ci4_savepoint_' . $savepoint . '__');

return $this->_savepointQuery($create, $commit);
}

/**
* Begin a Nested Transaction
*/
protected function _transBeginNested(): bool
{
return false !== $this->execute($this->_savepoint($this->transDepth + 1, true));

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for CodeIgniter\Database\MySQLi\Connection::_savepoint - expecting commit to be passed (see https://psalm.dev/025)

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for method CodeIgniter\Database\SavepointsForNestedTransactions::_savepoint saw 2 (see https://psalm.dev/025)

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for CodeIgniter\Database\OCI8\Connection::_savepoint - expecting commit to be passed (see https://psalm.dev/025)

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for CodeIgniter\Database\Postgre\Connection::_savepoint - expecting commit to be passed (see https://psalm.dev/025)

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for CodeIgniter\Database\SQLSRV\Connection::_savepoint - expecting commit to be passed (see https://psalm.dev/025)

Check failure on line 41 in system/Database/SavepointsForNestedTransactions.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis

TooFewArguments

system/Database/SavepointsForNestedTransactions.php:41:48: TooFewArguments: Too few arguments for CodeIgniter\Database\SQLite3\Connection::_savepoint - expecting commit to be passed (see https://psalm.dev/025)
}

/**
* Commit a Nested Transaction
*/
protected function _transCommitNested(): bool
{
return false !== $this->execute($this->_savepoint($this->transDepth, false, true));
}

/**
* Rollback a Nested Transaction
*/
protected function _transRollbackNested(): bool
{
return false !== $this->execute($this->_savepoint($this->transDepth, false, true));
}
}
15 changes: 15 additions & 0 deletions system/Test/Mock/MockConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,19 @@ protected function _transRollback(): bool
{
return true;
}

protected function _transBeginNested(): bool
{
return true;
}

protected function _transCommitNested(): bool
{
return true;
}

protected function _transRollbackNested(): bool
{
return true;
}
}
Loading
Loading