From 753765d52912f459e6eaf0020c697a6581c3690f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20K=C3=BCttel?= Date: Sat, 27 Apr 2024 19:52:06 +0200 Subject: [PATCH] feat(sqlite3): add support for nested transactions using SAVEPOINT It's possible to provide nested transaction semantics when using SQLite3 by using SAVEPOINT statements to manage transactions deeper than 1 level. The implementation starts a transaction when transBegin() is first called as before. But then all nested transactions will be managed by creating, releasing (commiting) or rolling back savepoint. Only when the outermost transaction is completed a COMMIT or ROLLBACK will be executed, either committing the transaction and all inner _committed_ transactions, or rolling everything back. - SQLite Transaction: https://sqlite.org/lang_transaction.html - SQLite Savepoints: https://sqlite.org/lang_savepoint.html --- system/Database/SQLite3/Connection.php | 61 ++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 945434184f33..a95289f8d57d 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -55,6 +55,13 @@ class Connection extends BaseConnection */ protected $busyTimeout; + /** + * How many savepoints have been created as part of managing nested transactions. + * + * @var int 0 = no savepoints, 1 = top level transaction + 0 savepoints, 2 = top level transaction + 1 savepoint, etc. + */ + protected $savepointLevel = 0; + public function initialize() { parent::initialize(); @@ -418,12 +425,32 @@ public function insertID(): int return $this->connID->lastInsertRowID(); } + /** + * Generates the SQL for managing savepoints, which + * are used to support nested transactions with SQLite3 + */ + private function _savepoint($savepoint, string $action = ''): string { + return ($action === '' ? '' : $action . ' ') . 'SAVEPOINT `__ci4_' . $savepoint . '`'; + } + + /** * Begin Transaction */ protected function _transBegin(): bool { - return $this->connID->exec('BEGIN TRANSACTION'); + $created = false; + $savepoint = $this->savepointLevel; + try { + + return $created = ($this->savepointLevel === 0 + ? $this->connID->exec('BEGIN TRANSACTION') + : $this->connID->exec($this->_savepoint($savepoint + 1))); + } finally { + if ($created) { + $this->savepointLevel++; + } + } } /** @@ -431,7 +458,22 @@ protected function _transBegin(): bool */ protected function _transCommit(): bool { - return $this->connID->exec('END TRANSACTION'); + if ($this->savepointLevel === 0) { + return false; + } + + $committed = false; + $savepoint = $this->savepointLevel; + try { + return $committed = ($this->savepointLevel <= 1 + ? $this->connID->exec('END TRANSACTION') + : $this->connID->exec($this->_savepoint($savepoint, 'RELEASE'))); + } finally { + if ($committed) { + $this->savepointLevel = max($this->savepointLevel - 1, 0); + } + + } } /** @@ -439,7 +481,20 @@ protected function _transCommit(): bool */ protected function _transRollback(): bool { - return $this->connID->exec('ROLLBACK'); + if ($this->savepointLevel === 0) { + return false; + } + $rolledBack = false; + $savepoint = $this->savepointLevel; + try { + return $rolledBack = ($this->savepointLevel <= 1 + ? $this->connID->exec('ROLLBACK') + : $this->connID->exec($this->_savepoint($savepoint, 'ROLLBACK TO'))); + } finally { + if ($rolledBack) { + $this->savepointLevel = max($this->savepointLevel - 1, 0); + } + } } /**