diff --git a/src/db/Capacitor.php b/src/db/Capacitor.php index 396fa17..0c2f161 100644 --- a/src/db/Capacitor.php +++ b/src/db/Capacitor.php @@ -1,11 +1,14 @@ storage = $storage; $this->channel = $channel; @@ -34,6 +37,80 @@ class Capacitor { 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 { return $this->storage->_getCreateSql($this->channel); } @@ -51,6 +128,7 @@ class Capacitor { } function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { + $this->beginTransaction(); 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 { + $this->beginTransaction(); return $this->storage->_each($this->channel, $filter, $func, $args); } function delete($filter, $func=null, ?array $args=null): int { + $this->beginTransaction(); return $this->storage->_delete($this->channel, $filter, $func, $args); } diff --git a/src/db/CapacitorStorage.php b/src/db/CapacitorStorage.php index 862f1e1..cb65f49 100644 --- a/src/db/CapacitorStorage.php +++ b/src/db/CapacitorStorage.php @@ -204,6 +204,7 @@ EOT; function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int { $this->_create($channel); $tableName = $channel->getTableName(); + $db = $this->db(); $initFunc = [$channel, "getItemValues"]; $initArgs = $args; @@ -217,7 +218,7 @@ EOT; $rowIds = $this->getRowIds($channel, $row, $primaryKeys); if ($rowIds !== null) { # modification - $prow = $this->db()->one([ + $prow = $db->one([ "select", "from" => $tableName, "where" => $rowIds, @@ -276,28 +277,41 @@ EOT; } } - if ($insert === null) { - # aucune modification - return 0; - } elseif ($insert) { - $id = $this->db()->exec([ - "insert", - "into" => $tableName, - "values" => $row, - ]); - if (count($primaryKeys) == 1 && $rowIds === null) { - # mettre à jour avec l'id généré - $values[$primaryKeys[0]] = $id; - } - } else { - $this->db()->exec([ - "update", - "table" => $tableName, - "values" => $row, - "where" => $rowIds, - ]); + # aucune modification + if ($insert === null) return 0; + + $manageTransactions = $channel->isManageTransactions(); + if ($manageTransactions) { + $commited = false; + $db->beginTransaction(); + } + try { + if ($insert) { + $id = $db->exec([ + "insert", + "into" => $tableName, + "values" => $row, + ]); + if (count($primaryKeys) == 1 && $rowIds === null) { + # mettre à jour avec l'id généré + $values[$primaryKeys[0]] = $id; + } + } else { + $db->exec([ + "update", + "table" => $tableName, + "values" => $row, + "where" => $rowIds, + ]); + } + if ($manageTransactions) { + $db->commit(); + $commited = true; + } + return 1; + } finally { + if ($manageTransactions && !$commited) $db->rollback(); } - return 1; } function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int { diff --git a/src/db/IDatabase.php b/src/db/IDatabase.php index b5a9047..64b0b0c 100644 --- a/src/db/IDatabase.php +++ b/src/db/IDatabase.php @@ -1,13 +1,7 @@ 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(); + 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 { $this->db()->commit(); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } } function rollback(): void { $this->db()->rollBack(); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->rollback(); + } + } } /** diff --git a/src/db/pdo/_query_base.php b/src/db/pdo/_query_base.php index f339f11..ad04447 100644 --- a/src/db/pdo/_query_base.php +++ b/src/db/pdo/_query_base.php @@ -82,7 +82,10 @@ class _query_base extends _base { } } + const DEBUG_QUERIES = false; + function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { + if (static::DEBUG_QUERIES) error_log($this->sql); #XXX if ($this->bindings !== null) { $stmt = $db->prepare($this->sql); foreach ($this->bindings as $name => $value) { diff --git a/src/db/sqlite/Sqlite.php b/src/db/sqlite/Sqlite.php index 99577f4..2c30763 100644 --- a/src/db/sqlite/Sqlite.php +++ b/src/db/sqlite/Sqlite.php @@ -4,6 +4,9 @@ namespace nur\sery\db\sqlite; use Generator; use nur\sery\cl; use nur\sery\db\IDatabase; +use nur\sery\db\ITransactor; +use nur\sery\php\func; +use nur\sery\ValueException; use SQLite3; use SQLite3Result; use SQLite3Stmt; @@ -82,6 +85,7 @@ class Sqlite implements IDatabase { $this->migration = $params["migrate"] ?? static::MIGRATE; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + $this->inTransaction = false; if ($params["auto_open"] ?? $defaultAutoOpen) { $this->open(); } @@ -113,11 +117,14 @@ class Sqlite implements IDatabase { /** @var SQLite3 */ protected $db; + protected bool $inTransaction; + function open(): self { if ($this->db === null) { $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); _config::with($this->config)->configure($this); _migration::with($this->migration)->migrate($this); + $this->inTransaction = false; } return $this; } @@ -126,6 +133,7 @@ class Sqlite implements IDatabase { if ($this->db !== null) { $this->db->close(); $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->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 { + $this->inTransaction = false; $this->db()->exec("commit"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } } 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) { diff --git a/src/db/sqlite/_query_base.php b/src/db/sqlite/_query_base.php index 58605fd..d2e3ceb 100644 --- a/src/db/sqlite/_query_base.php +++ b/src/db/sqlite/_query_base.php @@ -32,7 +32,10 @@ class _query_base extends _base { } } + const DEBUG_QUERIES = false; + function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { + if (static::DEBUG_QUERIES) error_log($this->sql); #XXX if ($this->bindings !== null) { /** @var SQLite3Stmt $stmt */ $stmt = SqliteException::check($db, $db->prepare($this->sql)); diff --git a/src/php/time/DateTime.php b/src/php/time/DateTime.php index 24310b8..c978663 100644 --- a/src/php/time/DateTime.php +++ b/src/php/time/DateTime.php @@ -233,6 +233,18 @@ class DateTime extends \DateTime { 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 { return $this->format(); } diff --git a/src/php/time/Elapsed.php b/src/php/time/Elapsed.php index 9d7ed6f..62d5d9e 100644 --- a/src/php/time/Elapsed.php +++ b/src/php/time/Elapsed.php @@ -10,14 +10,16 @@ class Elapsed { const DAY = 60 * 60 * 24; /** @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 { if ($prefix === null) $prefix = "depuis"; switch ($resolution) { - case self::RES_DAYS: return self::format_days($seconds, $prefix, $zero); - case self::RES_HOURS: return self::format_hours($seconds, $prefix, $zero); - case self::RES_MINUTES: return self::format_minutes($seconds, $prefix, $zero); + case self::RESOLUTION_DAYS: return self::format_days($seconds, $prefix, $zero); + case self::RESOLUTION_HOURS: return self::format_hours($seconds, $prefix, $zero); + case self::RESOLUTION_MINUTES: return self::format_minutes($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); } - 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(); $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(); $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(); $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) { - if ($resolution < self::RES_SECONDS) $resolution = self::RES_SECONDS; - elseif ($resolution > self::RES_DAYS) $resolution = self::RES_DAYS; + function __construct(int $seconds, ?int $resolution=null) { + $resolution ??= static::DEFAULT_RESOLUTION; + if ($resolution < self::RESOLUTION_SECONDS) $resolution = self::RESOLUTION_SECONDS; + elseif ($resolution > self::RESOLUTION_DAYS) $resolution = self::RESOLUTION_DAYS; $this->seconds = $seconds; $this->resolution = $resolution; }