2016-10-06 02:08:43 +00:00
|
|
|
<?php
|
2017-11-17 01:23:18 +00:00
|
|
|
/** @license MIT
|
|
|
|
* Copyright 2017 J. King, Dustin Wilson et al.
|
|
|
|
* See LICENSE and AUTHORS files for details */
|
|
|
|
|
2016-10-06 02:08:43 +00:00
|
|
|
declare(strict_types=1);
|
2021-04-14 15:17:01 +00:00
|
|
|
|
2017-03-28 04:12:12 +00:00
|
|
|
namespace JKingWeb\Arsse\Db;
|
2016-10-06 02:08:43 +00:00
|
|
|
|
2017-12-18 23:29:32 +00:00
|
|
|
use JKingWeb\Arsse\Arsse;
|
|
|
|
|
2017-03-03 01:47:00 +00:00
|
|
|
abstract class AbstractDriver implements Driver {
|
2019-01-11 00:01:32 +00:00
|
|
|
use SQLState;
|
|
|
|
|
2017-07-08 01:06:38 +00:00
|
|
|
protected $locked = false;
|
2017-02-16 20:29:42 +00:00
|
|
|
protected $transDepth = 0;
|
2017-05-07 22:27:16 +00:00
|
|
|
protected $transStatus = [];
|
2017-02-19 22:02:03 +00:00
|
|
|
|
2018-11-21 16:06:12 +00:00
|
|
|
abstract protected function lock(): bool;
|
|
|
|
abstract protected function unlock(bool $rollback = false): bool;
|
2019-01-11 00:01:32 +00:00
|
|
|
abstract protected static function buildEngineException($code, string $msg): array;
|
2017-12-19 17:11:49 +00:00
|
|
|
|
2024-12-15 21:31:57 +00:00
|
|
|
public function schemaUpdate(int $to, ?string $basePath = null): bool {
|
2017-12-18 23:29:32 +00:00
|
|
|
$ver = $this->schemaVersion();
|
|
|
|
if (!Arsse::$conf->dbAutoUpdate) {
|
|
|
|
throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
|
|
|
|
} elseif ($ver >= $to) {
|
|
|
|
throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
|
|
|
|
}
|
|
|
|
$sep = \DIRECTORY_SEPARATOR;
|
|
|
|
$path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep.static::schemaID().$sep;
|
|
|
|
// lock the database
|
|
|
|
$this->savepointCreate(true);
|
|
|
|
for ($a = $this->schemaVersion(); $a < $to; $a++) {
|
|
|
|
$this->savepointCreate();
|
|
|
|
try {
|
|
|
|
$file = $path.$a.".sql";
|
|
|
|
if (!file_exists($file)) {
|
|
|
|
throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
|
|
|
} elseif (!is_readable($file)) {
|
|
|
|
throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
|
|
|
}
|
|
|
|
$sql = @file_get_contents($file);
|
2020-03-01 20:16:50 +00:00
|
|
|
if ($sql === false) {
|
2017-12-18 23:29:32 +00:00
|
|
|
throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore
|
2020-03-01 20:16:50 +00:00
|
|
|
} elseif ($sql === "") {
|
2017-12-19 17:11:49 +00:00
|
|
|
throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
2017-12-18 23:29:32 +00:00
|
|
|
}
|
|
|
|
try {
|
|
|
|
$this->exec($sql);
|
|
|
|
} catch (\Throwable $e) {
|
2019-01-11 00:01:32 +00:00
|
|
|
throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $e->getMessage()]);
|
2017-12-18 23:29:32 +00:00
|
|
|
}
|
2020-03-01 20:16:50 +00:00
|
|
|
if ($this->schemaVersion() != $a + 1) {
|
2017-12-18 23:29:32 +00:00
|
|
|
throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
|
|
|
}
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
// undo any partial changes from the failed update
|
|
|
|
$this->savepointUndo();
|
|
|
|
// commit any successful updates if updating by more than one version
|
|
|
|
$this->savepointRelease();
|
|
|
|
// throw the error received
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
$this->savepointRelease();
|
|
|
|
}
|
|
|
|
$this->savepointRelease();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-07-08 01:06:38 +00:00
|
|
|
public function begin(bool $lock = false): Transaction {
|
|
|
|
return new Transaction($this, $lock);
|
2017-05-06 16:02:27 +00:00
|
|
|
}
|
2018-10-26 18:58:04 +00:00
|
|
|
|
2017-07-08 01:06:38 +00:00
|
|
|
public function savepointCreate(bool $lock = false): int {
|
2018-11-27 22:16:00 +00:00
|
|
|
// if no transaction is active and a lock was requested, lock the database using a backend-specific routine
|
2017-08-29 14:50:31 +00:00
|
|
|
if ($lock && !$this->transDepth) {
|
2017-07-08 01:06:38 +00:00
|
|
|
$this->lock();
|
|
|
|
$this->locked = true;
|
|
|
|
}
|
2018-12-20 23:06:28 +00:00
|
|
|
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
|
|
|
// if locks are not compatible with transactions (and savepoints), don't actually create a savepoint)
|
|
|
|
$this->transDepth++;
|
|
|
|
} else {
|
|
|
|
// create a savepoint, incrementing the transaction depth
|
|
|
|
$this->exec("SAVEPOINT arsse_".(++$this->transDepth));
|
|
|
|
}
|
2018-11-27 22:16:00 +00:00
|
|
|
// set the state of the newly created savepoint to pending
|
2017-05-07 22:27:16 +00:00
|
|
|
$this->transStatus[$this->transDepth] = self::TR_PEND;
|
2018-11-27 22:16:00 +00:00
|
|
|
// return the depth number
|
2017-05-07 22:27:16 +00:00
|
|
|
return $this->transDepth;
|
2017-02-16 20:29:42 +00:00
|
|
|
}
|
2016-10-06 02:08:43 +00:00
|
|
|
|
2024-12-15 21:31:57 +00:00
|
|
|
public function savepointRelease(?int $index = null): bool {
|
2018-11-27 22:16:00 +00:00
|
|
|
// assume the most recent savepoint if none was specified
|
2017-09-05 23:35:14 +00:00
|
|
|
$index = $index ?? $this->transDepth;
|
2017-08-29 14:50:31 +00:00
|
|
|
if (array_key_exists($index, $this->transStatus)) {
|
|
|
|
switch ($this->transStatus[$index]) {
|
2017-05-07 22:27:16 +00:00
|
|
|
case self::TR_PEND:
|
2018-12-20 23:06:28 +00:00
|
|
|
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
|
|
|
// if locks are not compatible with transactions, do nothing
|
|
|
|
} else {
|
|
|
|
// release the requested savepoint
|
|
|
|
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
|
|
|
}
|
|
|
|
// set its state to committed
|
2017-05-07 22:27:16 +00:00
|
|
|
$this->transStatus[$index] = self::TR_COMMIT;
|
2018-11-27 22:16:00 +00:00
|
|
|
// for any later pending savepoints, set their state to implicitly committed
|
2017-05-07 22:27:16 +00:00
|
|
|
$a = $index;
|
2017-08-29 14:50:31 +00:00
|
|
|
while (++$a && $a <= $this->transDepth) {
|
|
|
|
if ($this->transStatus[$a] <= self::TR_PEND) {
|
2017-07-21 02:40:09 +00:00
|
|
|
$this->transStatus[$a] = self::TR_PEND_COMMIT;
|
|
|
|
}
|
2017-05-07 22:27:16 +00:00
|
|
|
}
|
2018-11-27 22:16:00 +00:00
|
|
|
// return success
|
2017-05-07 22:27:16 +00:00
|
|
|
$out = true;
|
|
|
|
break;
|
|
|
|
case self::TR_PEND_COMMIT:
|
2018-11-27 22:16:00 +00:00
|
|
|
// set the state to explicitly committed
|
2017-05-07 22:27:16 +00:00
|
|
|
$this->transStatus[$index] = self::TR_COMMIT;
|
|
|
|
$out = true;
|
|
|
|
break;
|
|
|
|
case self::TR_PEND_ROLLBACK:
|
2018-11-27 22:16:00 +00:00
|
|
|
// set the state to explicitly committed
|
2017-05-07 22:27:16 +00:00
|
|
|
$this->transStatus[$index] = self::TR_COMMIT;
|
|
|
|
$out = false;
|
|
|
|
break;
|
|
|
|
case self::TR_COMMIT:
|
2017-07-22 19:29:12 +00:00
|
|
|
case self::TR_ROLLBACK: //@codeCoverageIgnore
|
2018-11-27 22:16:00 +00:00
|
|
|
// savepoint has already been released or rolled back; this is an error
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointStale", ['action' => "commit", 'index' => $index]);
|
2017-05-07 22:27:16 +00:00
|
|
|
default:
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointStatusUnknown", $this->transStatus[$index]); // @codeCoverageIgnore
|
2017-05-07 22:27:16 +00:00
|
|
|
}
|
2019-01-11 15:38:06 +00:00
|
|
|
if ($index == $this->transDepth) {
|
2018-11-27 22:16:00 +00:00
|
|
|
// if we've released the topmost savepoint, clean up all prior savepoints which have already been explicitly committed (or rolled back), if any
|
2017-08-29 14:50:31 +00:00
|
|
|
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
2017-05-07 22:27:16 +00:00
|
|
|
array_pop($this->transStatus);
|
|
|
|
$this->transDepth--;
|
|
|
|
}
|
|
|
|
}
|
2018-11-27 22:16:00 +00:00
|
|
|
// if no savepoints are pending and the database was locked, unlock it
|
2017-08-29 14:50:31 +00:00
|
|
|
if (!$this->transDepth && $this->locked) {
|
2017-07-08 01:06:38 +00:00
|
|
|
$this->unlock();
|
|
|
|
$this->locked = false;
|
|
|
|
}
|
2017-05-07 22:27:16 +00:00
|
|
|
return $out;
|
2017-02-16 20:29:42 +00:00
|
|
|
} else {
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointInvalid", ['action' => "commit", 'index' => $index]);
|
2017-02-16 20:29:42 +00:00
|
|
|
}
|
|
|
|
}
|
2016-10-06 02:08:43 +00:00
|
|
|
|
2024-12-15 21:31:57 +00:00
|
|
|
public function savepointUndo(?int $index = null): bool {
|
2017-09-05 23:35:14 +00:00
|
|
|
$index = $index ?? $this->transDepth;
|
2017-08-29 14:50:31 +00:00
|
|
|
if (array_key_exists($index, $this->transStatus)) {
|
|
|
|
switch ($this->transStatus[$index]) {
|
2017-05-07 22:27:16 +00:00
|
|
|
case self::TR_PEND:
|
2018-12-20 23:06:28 +00:00
|
|
|
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
|
|
|
// if locks are not compatible with transactions, do nothing and report failure as a rollback cannot occur
|
|
|
|
$out = false;
|
|
|
|
} else {
|
|
|
|
// roll back and then erase the savepoint
|
|
|
|
$this->exec("ROLLBACK TO SAVEPOINT arsse_".$index);
|
|
|
|
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
|
|
|
}
|
2017-05-07 22:27:16 +00:00
|
|
|
$this->transStatus[$index] = self::TR_ROLLBACK;
|
|
|
|
$a = $index;
|
2017-08-29 14:50:31 +00:00
|
|
|
while (++$a && $a <= $this->transDepth) {
|
|
|
|
if ($this->transStatus[$a] <= self::TR_PEND) {
|
2017-07-21 02:40:09 +00:00
|
|
|
$this->transStatus[$a] = self::TR_PEND_ROLLBACK;
|
|
|
|
}
|
2017-05-07 22:27:16 +00:00
|
|
|
}
|
2018-12-20 23:06:28 +00:00
|
|
|
$out = $out ?? true;
|
2017-05-07 22:27:16 +00:00
|
|
|
break;
|
|
|
|
case self::TR_PEND_COMMIT:
|
|
|
|
$this->transStatus[$index] = self::TR_ROLLBACK;
|
|
|
|
$out = false;
|
|
|
|
break;
|
|
|
|
case self::TR_PEND_ROLLBACK:
|
|
|
|
$this->transStatus[$index] = self::TR_ROLLBACK;
|
|
|
|
$out = true;
|
|
|
|
break;
|
|
|
|
case self::TR_COMMIT:
|
2017-07-22 19:29:12 +00:00
|
|
|
case self::TR_ROLLBACK: //@codeCoverageIgnore
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointStale", ['action' => "rollback", 'index' => $index]);
|
2017-05-07 22:27:16 +00:00
|
|
|
default:
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointStatusUnknown", $this->transStatus[$index]); // @codeCoverageIgnore
|
2017-05-07 22:27:16 +00:00
|
|
|
}
|
2019-01-11 15:38:06 +00:00
|
|
|
if ($index == $this->transDepth) {
|
2017-08-29 14:50:31 +00:00
|
|
|
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
2017-05-07 22:27:16 +00:00
|
|
|
array_pop($this->transStatus);
|
|
|
|
$this->transDepth--;
|
|
|
|
}
|
|
|
|
}
|
2017-08-29 14:50:31 +00:00
|
|
|
if (!$this->transDepth && $this->locked) {
|
2017-07-08 01:06:38 +00:00
|
|
|
$this->unlock(true);
|
|
|
|
$this->locked = false;
|
|
|
|
}
|
2017-05-07 22:27:16 +00:00
|
|
|
return $out;
|
2017-02-16 20:29:42 +00:00
|
|
|
} else {
|
2017-11-06 03:13:44 +00:00
|
|
|
throw new Exception("savepointInvalid", ['action' => "rollback", 'index' => $index]);
|
2017-02-16 20:29:42 +00:00
|
|
|
}
|
|
|
|
}
|
2016-10-06 02:08:43 +00:00
|
|
|
|
2017-07-07 15:49:54 +00:00
|
|
|
public function prepare(string $query, ...$paramType): Statement {
|
2017-02-16 20:29:42 +00:00
|
|
|
return $this->prepareArray($query, $paramType);
|
|
|
|
}
|
2017-08-29 14:50:31 +00:00
|
|
|
}
|