FIX transaction depth related errors with invalid savepoint names

The logic for cancelling a savepoint was incorrect, as the behaviour
the logic was modelled on was for a different RDBMS - where a COMMIT
would always close the most recently opened transaction.

SQLite on contrast will commit the entire transaction, not just the
most recent savepoint marker until current execution point. The correct
manner to deal with a 'partial' commit is to call RELEASE <savepoint>.

This revealed an error in the savepoint logic, in that if someone had
supplied a savepoint name instead of relying on generated ones, the
rollback command did not factor for this and always assumed generated
savepoint names - again causing error. For this reason a new class
member field has been introduced to track savepoint names in a stack
fashion.
This commit is contained in:
NightjarNZ 2018-10-11 21:51:32 +13:00
parent 62ef14f711
commit 0fa6b0fde7
1 changed files with 54 additions and 12 deletions

View File

@ -65,6 +65,11 @@ class SQLite3Database extends Database
*/
protected $transactionNesting = 0;
/**
* @var array
*/
protected $transactionSavepoints = [];
/**
* List of default pragma values
*
@ -471,28 +476,27 @@ class SQLite3Database extends Database
public function transactionStart($transaction_mode = false, $session_characteristics = false)
{
if ($this->transactionDepth()) {
$this->transactionSavepoint($this->getTransactionSavepointName());
$this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionDepth());
} else {
$this->query('BEGIN');
$this->transactionDepthIncrease();
}
$this->transactionDepthIncrease();
}
public function transactionSavepoint($savepoint)
{
$this->query("SAVEPOINT \"$savepoint\"");
$this->transactionDepthIncrease($savepoint);
}
/**
* Fetch the name of the current savepoint
* {@see transactionDepth} should be greater than zero
* or the name will be invalid (because there are none).
* Fetch the name of the most recent savepoint
*
* @return string
*/
protected function getTransactionSavepointName()
{
return 'NESTEDTRANSACTION' . $this->transactionDepth();
return end($this->transactionSavepoints);
}
public function transactionRollback($savepoint = false)
@ -500,6 +504,7 @@ class SQLite3Database extends Database
// Named transaction
if ($savepoint) {
$this->query("ROLLBACK TO $savepoint;");
$this->transactionDepthDecrease();
return true;
}
@ -508,11 +513,11 @@ class SQLite3Database extends Database
return false;
}
$this->transactionDepthDecrease();
if ($this->transactionDepth()) {
if ($this->transactionIsNested()) {
$this->transactionRollback($this->getTransactionSavepointName());
} else {
$this->query('ROLLBACK;');
$this->transactionDepthDecrease();
}
return true;
}
@ -528,24 +533,60 @@ class SQLite3Database extends Database
if (!$this->transactionDepth()) {
return false;
}
$this->query('COMMIT;');
$this->transactionDepthDecrease();
if ($this->transactionIsNested()) {
$savepoint = $this->getTransactionSavepointName();
$this->query('RELEASE ' . $savepoint);
$this->transactionDepthDecrease();
} else {
$this->query('COMMIT;');
$this->resetTransactionNesting();
}
if ($chain) {
$this->transactionStart();
}
return true;
}
/**
* Increase the nested transaction level by one
* Indicate whether or not the current transaction is nested
* Returns false if there are no transactions, or the open
* transaction is the 'outer' transaction, i.e. not nested.
*
* @return bool
*/
protected function transactionDepthIncrease()
protected function transactionIsNested()
{
return $this->transactionNesting > 1;
}
/**
* Increase the nested transaction level by one
* savepoint tracking is optional because BEGIN
* opens a transaction, but is not a named reference
*
* @param string $savepoint
*/
protected function transactionDepthIncrease($savepoint = null)
{
++$this->transactionNesting;
if ($savepoint) {
array_push($this->transactionSavepoints, $savepoint);
}
}
/**
* Decrease the nested transaction level by one
* and reduce the savepoint tracking if we are
* nesting, as the last one is no longer valid
*/
protected function transactionDepthDecrease()
{
if ($this->transactionIsNested()) {
array_pop($this->transactionSavepoints);
}
--$this->transactionNesting;
}
@ -555,6 +596,7 @@ class SQLite3Database extends Database
protected function resetTransactionNesting()
{
$this->transactionNesting = 0;
$this->transactionSavepoints = [];
}
public function query($sql, $errorLevel = E_USER_ERROR)