ajout transactor

This commit is contained in:
Jephté Clain 2024-06-20 12:36:46 +04:00
parent ff6c5e8da4
commit ad603e8e81
10 changed files with 279 additions and 46 deletions

View File

@ -1,11 +1,14 @@
<?php <?php
namespace nur\sery\db; namespace nur\sery\db;
use nur\sery\php\func;
use nur\sery\ValueException;
/** /**
* Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une * Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une
* instance de {@link CapacitorStorage} * instance de {@link CapacitorStorage}
*/ */
class Capacitor { class Capacitor implements ITransactor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) { function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) {
$this->storage = $storage; $this->storage = $storage;
$this->channel = $channel; $this->channel = $channel;
@ -34,6 +37,80 @@ class Capacitor {
return $this->getChannel()->getTableName(); return $this->getChannel()->getTableName();
} }
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this->channel;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw ValueException::invalid_type($channel, CapacitorChannel::class);
}
}
}
return $this;
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null): void {
if ($this->subManageTransactions === null && $this->subChannels !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
}
$channel->setManageTransactions(false);
}
$db = $this->db();
if (!$db->inTransaction()) $db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
$this->commit();
$commited = true;
} finally {
if (!$commited) $this->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
}
}
function commit(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $this->db()->commit();
}
function rollback(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $this->db()->rollback();
}
function getCreateSql(): string { function getCreateSql(): string {
return $this->storage->_getCreateSql($this->channel); return $this->storage->_getCreateSql($this->channel);
} }
@ -51,6 +128,7 @@ class Capacitor {
} }
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
$this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $values); return $this->storage->_charge($this->channel, $item, $func, $args, $values);
} }
@ -71,10 +149,12 @@ class Capacitor {
} }
function each($filter, $func=null, ?array $args=null): int { function each($filter, $func=null, ?array $args=null): int {
$this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args); return $this->storage->_each($this->channel, $filter, $func, $args);
} }
function delete($filter, $func=null, ?array $args=null): int { function delete($filter, $func=null, ?array $args=null): int {
$this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args); return $this->storage->_delete($this->channel, $filter, $func, $args);
} }

View File

@ -204,6 +204,7 @@ EOT;
function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int { function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int {
$this->_create($channel); $this->_create($channel);
$tableName = $channel->getTableName(); $tableName = $channel->getTableName();
$db = $this->db();
$initFunc = [$channel, "getItemValues"]; $initFunc = [$channel, "getItemValues"];
$initArgs = $args; $initArgs = $args;
@ -217,7 +218,7 @@ EOT;
$rowIds = $this->getRowIds($channel, $row, $primaryKeys); $rowIds = $this->getRowIds($channel, $row, $primaryKeys);
if ($rowIds !== null) { if ($rowIds !== null) {
# modification # modification
$prow = $this->db()->one([ $prow = $db->one([
"select", "select",
"from" => $tableName, "from" => $tableName,
"where" => $rowIds, "where" => $rowIds,
@ -276,11 +277,17 @@ EOT;
} }
} }
if ($insert === null) {
# aucune modification # aucune modification
return 0; if ($insert === null) return 0;
} elseif ($insert) {
$id = $this->db()->exec([ $manageTransactions = $channel->isManageTransactions();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
try {
if ($insert) {
$id = $db->exec([
"insert", "insert",
"into" => $tableName, "into" => $tableName,
"values" => $row, "values" => $row,
@ -290,14 +297,21 @@ EOT;
$values[$primaryKeys[0]] = $id; $values[$primaryKeys[0]] = $id;
} }
} else { } else {
$this->db()->exec([ $db->exec([
"update", "update",
"table" => $tableName, "table" => $tableName,
"values" => $row, "values" => $row,
"where" => $rowIds, "where" => $rowIds,
]); ]);
} }
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return 1; return 1;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
} }
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int { function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int {

View File

@ -1,13 +1,7 @@
<?php <?php
namespace nur\sery\db; namespace nur\sery\db;
interface IDatabase { interface IDatabase extends ITransactor {
function beginTransaction(): void;
function commit(): void;
function rollback(): void;
/** /**
* - si c'est un insert, retourner l'identifiant autogénéré de la ligne * - si c'est un insert, retourner l'identifiant autogénéré de la ligne
* - sinon retourner le nombre de lignes modifiées en cas de succès, ou false * - sinon retourner le nombre de lignes modifiées en cas de succès, ou false

22
src/db/ITransactor.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace nur\sery\db;
/**
* Class ITransactor: un objet qui peut faire des opérations dans une
* transaction
*/
interface ITransactor {
/**
* Indiquer qu'une transaction va être étendue à tous les objets mentionnés
*/
function willUpdate(...$transactors): self;
function inTransaction(): bool;
/** si $func!==null, la lancer puis commiter la transaction */
function beginTransaction(?callable $func=null): void;
function commit(): void;
function rollback(): void;
}

View File

@ -4,9 +4,11 @@ namespace nur\sery\db\pdo;
use Generator; use Generator;
use nur\sery\cl; use nur\sery\cl;
use nur\sery\db\IDatabase; use nur\sery\db\IDatabase;
use nur\sery\db\ITransactor;
use nur\sery\php\func; use nur\sery\php\func;
use nur\sery\php\time\Date; use nur\sery\php\time\Date;
use nur\sery\php\time\DateTime; use nur\sery\php\time\DateTime;
use nur\sery\ValueException;
class Pdo implements IDatabase { class Pdo implements IDatabase {
static function with($pdo, ?array $params=null): self { static function with($pdo, ?array $params=null): self {
@ -144,16 +146,60 @@ class Pdo implements IDatabase {
} }
} }
function beginTransaction(): void { /** @var ITransactor[] */
protected ?array $transactors = null;
function willUpdate(...$transactors): self {
foreach ($transactors as $transactor) {
if ($transactor instanceof ITransactor) {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null): void {
$this->db()->beginTransaction(); $this->db()->beginTransaction();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
$this->commit();
$commited = true;
} finally {
if (!$commited) $this->rollback();
}
}
} }
function commit(): void { function commit(): void {
$this->db()->commit(); $this->db()->commit();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
} }
function rollback(): void { function rollback(): void {
$this->db()->rollBack(); $this->db()->rollBack();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
} }
/** /**

View File

@ -82,7 +82,10 @@ class _query_base extends _base {
} }
} }
const DEBUG_QUERIES = false;
function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) error_log($this->sql); #XXX
if ($this->bindings !== null) { if ($this->bindings !== null) {
$stmt = $db->prepare($this->sql); $stmt = $db->prepare($this->sql);
foreach ($this->bindings as $name => $value) { foreach ($this->bindings as $name => $value) {

View File

@ -4,6 +4,9 @@ namespace nur\sery\db\sqlite;
use Generator; use Generator;
use nur\sery\cl; use nur\sery\cl;
use nur\sery\db\IDatabase; use nur\sery\db\IDatabase;
use nur\sery\db\ITransactor;
use nur\sery\php\func;
use nur\sery\ValueException;
use SQLite3; use SQLite3;
use SQLite3Result; use SQLite3Result;
use SQLite3Stmt; use SQLite3Stmt;
@ -82,6 +85,7 @@ class Sqlite implements IDatabase {
$this->migration = $params["migrate"] ?? static::MIGRATE; $this->migration = $params["migrate"] ?? static::MIGRATE;
# #
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false;
if ($params["auto_open"] ?? $defaultAutoOpen) { if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open(); $this->open();
} }
@ -113,11 +117,14 @@ class Sqlite implements IDatabase {
/** @var SQLite3 */ /** @var SQLite3 */
protected $db; protected $db;
protected bool $inTransaction;
function open(): self { function open(): self {
if ($this->db === null) { if ($this->db === null) {
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
_config::with($this->config)->configure($this); _config::with($this->config)->configure($this);
_migration::with($this->migration)->migrate($this); _migration::with($this->migration)->migrate($this);
$this->inTransaction = false;
} }
return $this; return $this;
} }
@ -126,6 +133,7 @@ class Sqlite implements IDatabase {
if ($this->db !== null) { if ($this->db !== null) {
$this->db->close(); $this->db->close();
$this->db = null; $this->db = null;
$this->inTransaction = false;
} }
} }
@ -172,16 +180,64 @@ class Sqlite implements IDatabase {
} }
} }
function beginTransaction(): void { /** @var ITransactor[] */
protected ?array $transactors = null;
function willUpdate(...$transactors): self {
foreach ($transactors as $transactor) {
if ($transactor instanceof ITransactor) {
$this->transactors[] = $transactor;
$transactor->willUpdate();
} else {
throw ValueException::invalid_type($transactor, ITransactor::class);
}
}
return $this;
}
function inTransaction(): bool {
#XXX très imparfait, mais y'a rien de mieux pour le moment :-(
return $this->inTransaction;
}
function beginTransaction(?callable $func=null): void {
$this->db()->exec("begin"); $this->db()->exec("begin");
$this->inTransaction = true;
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
$this->commit();
$commited = true;
} finally {
if (!$commited) $this->rollback();
}
}
} }
function commit(): void { function commit(): void {
$this->inTransaction = false;
$this->db()->exec("commit"); $this->db()->exec("commit");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
} }
function rollback(): void { function rollback(): void {
$this->db()->exec("commit"); $this->inTransaction = false;
$this->db()->exec("rollback");
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
} }
function _get(string $query, bool $entireRow=false) { function _get(string $query, bool $entireRow=false) {

View File

@ -32,7 +32,10 @@ class _query_base extends _base {
} }
} }
const DEBUG_QUERIES = false;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) error_log($this->sql); #XXX
if ($this->bindings !== null) { if ($this->bindings !== null) {
/** @var SQLite3Stmt $stmt */ /** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql)); $stmt = SqliteException::check($db, $db->prepare($this->sql));

View File

@ -233,6 +233,18 @@ class DateTime extends \DateTime {
return static::with($this->add(new \DateInterval("P${nbDays}D"))); return static::with($this->add(new \DateInterval("P${nbDays}D")));
} }
function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_at($this, $now, $resolution);
}
function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_since($this, $now, $resolution);
}
function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_delay($this, $now, $resolution);
}
function __toString(): string { function __toString(): string {
return $this->format(); return $this->format();
} }

View File

@ -10,14 +10,16 @@ class Elapsed {
const DAY = 60 * 60 * 24; const DAY = 60 * 60 * 24;
/** @var int résolution */ /** @var int résolution */
const RES_SECONDS = 0, RES_MINUTES = 1, RES_HOURS = 2, RES_DAYS = 3; const RESOLUTION_SECONDS = 0, RESOLUTION_MINUTES = 1, RESOLUTION_HOURS = 2, RESOLUTION_DAYS = 3;
const DEFAULT_RESOLUTION = self::RESOLUTION_SECONDS;
private static function format(int $seconds, int $resolution, ?string $prefix=null, ?string $zero=null): string { private static function format(int $seconds, int $resolution, ?string $prefix=null, ?string $zero=null): string {
if ($prefix === null) $prefix = "depuis"; if ($prefix === null) $prefix = "depuis";
switch ($resolution) { switch ($resolution) {
case self::RES_DAYS: return self::format_days($seconds, $prefix, $zero); case self::RESOLUTION_DAYS: return self::format_days($seconds, $prefix, $zero);
case self::RES_HOURS: return self::format_hours($seconds, $prefix, $zero); case self::RESOLUTION_HOURS: return self::format_hours($seconds, $prefix, $zero);
case self::RES_MINUTES: return self::format_minutes($seconds, $prefix, $zero); case self::RESOLUTION_MINUTES: return self::format_minutes($seconds, $prefix, $zero);
default: return self::format_seconds($seconds, $prefix, $zero); default: return self::format_seconds($seconds, $prefix, $zero);
} }
} }
@ -122,27 +124,28 @@ class Elapsed {
return self::format_generic($prefix, $d, 0, 0); return self::format_generic($prefix, $d, 0, 0);
} }
static function format_at(DateTime $start, ?DateTime $now=null): string { static function format_at(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime(); $now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp(); $seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatAt(); return (new self($seconds, $resolution))->formatAt();
} }
static function format_since(DateTime $start, ?DateTime $now=null): string { static function format_since(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime(); $now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp(); $seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatSince(); return (new self($seconds, $resolution))->formatSince();
} }
static function format_delay(DateTime $start, ?DateTime $now=null): string { static function format_delay(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime(); $now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp(); $seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatDelay(); return (new self($seconds, $resolution))->formatDelay();
} }
function __construct(int $seconds, int $resolution=self::RES_SECONDS) { function __construct(int $seconds, ?int $resolution=null) {
if ($resolution < self::RES_SECONDS) $resolution = self::RES_SECONDS; $resolution ??= static::DEFAULT_RESOLUTION;
elseif ($resolution > self::RES_DAYS) $resolution = self::RES_DAYS; if ($resolution < self::RESOLUTION_SECONDS) $resolution = self::RESOLUTION_SECONDS;
elseif ($resolution > self::RESOLUTION_DAYS) $resolution = self::RESOLUTION_DAYS;
$this->seconds = $seconds; $this->seconds = $seconds;
$this->resolution = $resolution; $this->resolution = $resolution;
} }