From 907f17af334887e17ad97bcfeb86f205ad8f2dd1 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 20 May 2024 10:46:18 +0400 Subject: [PATCH] modifs.mineures sans commentaires --- src/db/AbstractCapacitor.php | 45 +++----- src/db/Capacitor.php | 16 +-- src/db/CapacitorChannel.php | 14 +++ src/db/ICapacitor.php | 29 +++-- src/db/sqlite/SqliteCapacitor.php | 143 +++++++++++++++++------- tests/db/sqlite/SqliteCapacitorTest.php | 68 ++++++++--- 6 files changed, 212 insertions(+), 103 deletions(-) diff --git a/src/db/AbstractCapacitor.php b/src/db/AbstractCapacitor.php index d951562..8d1ac7b 100644 --- a/src/db/AbstractCapacitor.php +++ b/src/db/AbstractCapacitor.php @@ -5,59 +5,44 @@ namespace nur\sery\db; * Class AbstractCapacitor: implémentation de base d'un {@link ICapacitor} */ abstract class AbstractCapacitor implements ICapacitor { - abstract protected function getChannel(?string $name=null): CapacitorChannel; + abstract protected function getChannel(?string $name): CapacitorChannel; abstract function _exists(CapacitorChannel $channel): bool; /** tester si le canal spécifié existe */ - function exists(?string $channel=null): bool { + function exists(?string $channel): bool { return $this->_exists($this->getChannel($channel)); } abstract function _reset(CapacitorChannel $channel): void; /** supprimer le canal spécifié */ - function reset(?string $channel=null): void { + function reset(?string $channel): void { $this->_reset($this->getChannel($channel)); } - abstract function _charge($item, CapacitorChannel $channel): void; + abstract function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): bool; - /** charger une valeur dans le canal */ - function charge($item, ?string $channel=null): void { - $this->_charge($item, $this->getChannel($channel)); + function charge(?string $channel, $item, ?callable $func=null, ?array $args=null): bool { + return $this->_charge($this->getChannel($channel), $item, $func, $args); } - abstract function _discharge($keys=null, CapacitorChannel $channel=null, ?bool $reset=null): iterable; + abstract function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable; - /** décharger les données du canal spécifié */ - function discharge($keys=null, ?string $channel=null, ?bool $reset=null): iterable { - return $this->_discharge($keys, $this->getChannel($channel), $reset); + function discharge(?string $channel, $filter=null, ?bool $reset=null): iterable { + return $this->_discharge($this->getChannel($channel), $filter, $reset); } - abstract function _get($keys, CapacitorChannel $channel=null); + abstract function _get(CapacitorChannel $channel, $filter); - /** - * obtenir l'élément identifié par les clés spécifiées sur le canal spécifié - * - * si $keys n'est pas un tableau, il est transformé en ["_id" => $keys] - */ - function get($keys, ?string $channel=null) { - return $this->_get($keys, $this->getChannel($channel)); + function get(?string $channel, $filter) { + return $this->_get($this->getChannel($channel), $filter); } - abstract function _each($keys, callable $func, ?array $args=null, CapacitorChannel $channel=null): void; + abstract function _each(CapacitorChannel $channel, $filter, callable $func, ?array $args): void; - /** - * appeler une fonction pour chaque élément du canal spécifié. - * - * $keys permet de filtrer parmi les élements chargés - * - * si $func retourne un tableau, il est utilisé pour mettre à jour - * l'enregistrement. - */ - function each($keys, callable $func, ?array $args=null, ?string $channel=null): void { - $this->_each($keys, $func, $args, $this->getChannel($channel)); + function each(?string $channel, $filter, callable $func, ?array $args=null): void { + $this->_each($this->getChannel($channel), $filter, $func, $args); } abstract function close(): void; diff --git a/src/db/Capacitor.php b/src/db/Capacitor.php index d2a3ff3..a0918c5 100644 --- a/src/db/Capacitor.php +++ b/src/db/Capacitor.php @@ -25,20 +25,20 @@ class Capacitor { $this->capacitor->_reset($this->channel); } - function charge($item) { - $this->capacitor->_charge($item, $this->channel); + function charge($item, ?callable $func=null, ?array $args=null): bool { + return $this->capacitor->_charge($this->channel, $item, $func, $args); } - function discharge($keys=null, ?bool $reset=null): iterable { - return $this->capacitor->_discharge($keys, $this->channel, $reset); + function discharge($filter=null, ?bool $reset=null): iterable { + return $this->capacitor->_discharge($this->channel, $filter, $reset); } - function get($keys) { - return $this->capacitor->_get($keys, $this->channel); + function get($filter) { + return $this->capacitor->_get($this->channel, $filter); } - function each($keys, callable $func, ?array $args=null): void { - $this->capacitor->_each($keys, $func, $args, $this->channel); + function each($filter, callable $func, ?array $args=null): void { + $this->capacitor->_each($this->channel, $filter, $func, $args); } function close(): void { diff --git a/src/db/CapacitorChannel.php b/src/db/CapacitorChannel.php index 2158120..23bd043 100644 --- a/src/db/CapacitorChannel.php +++ b/src/db/CapacitorChannel.php @@ -38,10 +38,24 @@ class CapacitorChannel { $this->created = $created; } + /** + * retourner un ensemble de définitions pour des colonnes supplémentaires à + * insérer lors du chargement d'une valeur + * + * la colonne "_id" de définition "integer primary key autoincrement" est la + * clé primaire par défaut. elle peut être redéfinie, et dans ce cas la valeur + * à utiliser doit être retournée par {@link getKeyValues()} + */ function getKeyDefinitions(): ?array { return null; } + /** + * calculer les valeurs des colonnes supplémentaires à insérer pour le + * chargement de $item + * + * Si "_id" est retourné, la ligne existante est mise à jour le cas échéant. + */ function getKeyValues($item): ?array { return null; } diff --git a/src/db/ICapacitor.php b/src/db/ICapacitor.php index 9ff0c4a..6b935e2 100644 --- a/src/db/ICapacitor.php +++ b/src/db/ICapacitor.php @@ -7,33 +7,44 @@ namespace nur\sery\db; */ interface ICapacitor { /** tester si le canal spécifié existe */ - function exists(?string $channel=null): bool; + function exists(?string $channel): bool; /** supprimer le canal spécifié */ - function reset(?string $channel=null): void; + function reset(?string $channel): void; - /** charger une valeur dans le canal */ - function charge($item, ?string $channel=null): void; + /** + * charger une valeur dans le canal + * + * Si $func!==null, après avoir calculé les valeurs des clés supplémentaires + * avec {@link CapacitorChannel::getKeyValues()}, la fonction est appelée avec + * les arguments ($item, $keyValues, $row, ...$args) + * Si la fonction retourne un tableau, il est utilisé pour modifié les valeurs + * insérées/mises à jour + * + * @return true si l'objet a été chargé ou mis à jour, false s'il existait + * déjà à l'identique dans le canal + */ + function charge(?string $channel, $item, ?callable $func=null, ?array $args=null): bool; /** décharger les données du canal spécifié */ - function discharge($keys=null, ?string $channel=null, ?bool $reset=null): iterable; + function discharge(?string $channel, $filter=null, ?bool $reset=null): iterable; /** * obtenir l'élément identifié par les clés spécifiées sur le canal spécifié * - * si $keys n'est pas un tableau, il est transformé en ["_id" => $keys] + * si $filter n'est pas un tableau, il est transformé en ["_id" => $filter] */ - function get($keys, ?string $channel=null); + function get(?string $channel, $filter); /** * appeler une fonction pour chaque élément du canal spécifié. * - * $keys permet de filtrer parmi les élements chargés + * $filter permet de filtrer parmi les élements chargés * * si $func retourne un tableau, il est utilisé pour mettre à jour * l'enregistrement. */ - function each($keys, callable $func, ?array $args=null, ?string $channel=null): void; + function each(?string $channel, $filter, callable $func, ?array $args=null): void; function close(): void; } diff --git a/src/db/sqlite/SqliteCapacitor.php b/src/db/sqlite/SqliteCapacitor.php index e8176db..2943ca7 100644 --- a/src/db/sqlite/SqliteCapacitor.php +++ b/src/db/sqlite/SqliteCapacitor.php @@ -27,6 +27,9 @@ class SqliteCapacitor extends AbstractCapacitor{ $columns = cl::merge([ "_id" => "integer primary key autoincrement", "_item" => "text", + "_sum" => "varchar(40)", + "_created" => "datetime", + "_modified" => "datetime", ], $channel->getKeyDefinitions()); $this->sqlite->exec([ "create table if not exists", @@ -46,7 +49,7 @@ class SqliteCapacitor extends AbstractCapacitor{ return $channel; } - protected function getChannel(?string $name=null): CapacitorChannel { + protected function getChannel(?string $name): CapacitorChannel { $name = CapacitorChannel::verifix_name($name); $channel = $this->channels[$name] ?? null; if ($channel === null) { @@ -73,25 +76,81 @@ class SqliteCapacitor extends AbstractCapacitor{ $channel->setCreated(false); } - function _charge($item, CapacitorChannel $channel): void { + function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): bool { $this->_create($channel); - $values = cl::merge($channel->getKeyValues($item), [ - "_item" => serialize($item), - ]); - $this->sqlite->exec([ - "insert", - "into" => $channel->getTableName(), - "values" => $values, - ]); + $now = date("Y-m-d H:i:s"); + $_item = serialize($item); + $_sum = sha1($_item); + $values = cl::merge([ + "_item" => $_item, + "_sum" => $_sum, + ], $channel->getKeyValues($item)); + $row = null; + $id = $values["_id"] ?? null; + if ($id !== null) { + # modification + $row = $this->sqlite->one([ + "select _item, _sum, _created, _modified", + "from" => $channel->getTableName(), + "where" => ["_id" => $id], + ]); + } + $insert = null; + if ($row === null) { + # création + $values = cl::merge($values, [ + "_created" => $now, + "_modified" => $now, + ]); + $insert = true; + } elseif ($_sum !== $row["_sum"]) { + # modification + $values = cl::merge($values, [ + "_modified" => $now, + ]); + $insert = false; + } + if ($func !== null) { + $context = func::_prepare($func); + $args ??= []; + $updates = func::_call($context, [$item, $values, $row, ...$args]); + if (is_array($updates)) { + if (array_key_exists("_item", $updates)) { + $updates["_item"] = serialize($updates["_item"]); + if (!array_key_exists("_modified", $updates)) { + $updates["_modified"] = $now; + } + } + $values = cl::merge($values, $updates); + } + } + if ($insert === null) { + # aucune modification + return false; + } elseif ($insert) { + $this->sqlite->exec([ + "insert", + "into" => $channel->getTableName(), + "values" => $values, + ]); + } else { + $this->sqlite->exec([ + "update", + "table" => $channel->getTableName(), + "values" => $values, + "where" => ["_id" => $id], + ]); + } + return true; } - function _discharge($keys=null, CapacitorChannel $channel=null, ?bool $reset=null): iterable { - if ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys]; - if ($reset === null) $reset = $keys === null; + function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable { + if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter]; + if ($reset === null) $reset = $filter === null; $rows = $this->sqlite->all([ "select _item", "from" => $channel->getTableName(), - "where" => $keys, + "where" => $filter, ]); foreach ($rows as $row) { $item = unserialize($row['_item']); @@ -100,43 +159,51 @@ class SqliteCapacitor extends AbstractCapacitor{ if ($reset) $this->_reset($channel); } - function _get($keys, CapacitorChannel $channel=null) { - if ($keys === null) throw ValueException::null("keys"); - if (!is_array($keys)) $keys = ["_id" => $keys]; + function _get(CapacitorChannel $channel, $filter) { + if ($filter === null) throw ValueException::null("keys"); + if (!is_array($filter)) $filter = ["_id" => $filter]; $row = $this->sqlite->one([ "select _item", "from" => $channel->getTableName(), - "where" => $keys, + "where" => $filter, ]); if ($row === null) return null; else return unserialize($row["_item"]); } - function _each($keys, callable $func, ?array $args=null, CapacitorChannel $channel=null): void { - if ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys]; + function _each(CapacitorChannel $channel, $filter, callable $func, ?array $args): void { + if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter]; $context = func::_prepare($func); $sqlite = $this->sqlite; $tableName = $channel->getTableName(); - $rows = $sqlite->all([ - "select", - "from" => $tableName, - "where" => $keys, - ]); - $args ??= []; - foreach ($rows as $row) { - $item = unserialize($row['_item']); - $updates = func::_call($context, [$item, $row, ...$args]); - if (is_array($updates)) { - if (array_key_exists("_item", $updates)) { - $updates["_item"] = serialize($updates["_item"]); + $commited = false; + $sqlite->beginTransaction(); + try { + $rows = $sqlite->all([ + "select", + "from" => $tableName, + "where" => $filter, + ]); + $args ??= []; + foreach ($rows as $row) { + $item = unserialize($row['_item']); + $updates = func::_call($context, [$item, $row, ...$args]); + if (is_array($updates)) { + if (array_key_exists("_item", $updates)) { + $updates["_item"] = serialize($updates["_item"]); + } + $sqlite->exec([ + "update", + "table" => $tableName, + "values" => $updates, + "where" => ["_id" => $row["_id"]], + ]); } - $sqlite->exec([ - "update", - "table" => $tableName, - "values" => $updates, - "where" => ["_id" => $row["_id"]], - ]); } + $sqlite->commit(); + $commited = true; + } finally { + if (!$commited) $sqlite->rollback(); } } diff --git a/tests/db/sqlite/SqliteCapacitorTest.php b/tests/db/sqlite/SqliteCapacitorTest.php index 134d610..704e5c6 100644 --- a/tests/db/sqlite/SqliteCapacitorTest.php +++ b/tests/db/sqlite/SqliteCapacitorTest.php @@ -1,23 +1,25 @@ reset($channel); - $capacitor->charge("first", $channel); - $capacitor->charge("second", $channel); - $capacitor->charge("third", $channel); - $items = iterator_to_array($capacitor->discharge(null, $channel, false)); + $capacitor->charge($channel, "first"); + $capacitor->charge($channel, "second"); + $capacitor->charge($channel, "third"); + $items = iterator_to_array($capacitor->discharge($channel, null, false)); self::assertSame(["first", "second", "third"], $items); } + function _testChargeArrays(SqliteCapacitor $capacitor, ?string $channel) { $capacitor->reset($channel); - $capacitor->charge(["id" => 10, "name" => "first"], $channel); - $capacitor->charge(["name" => "second", "id" => 20], $channel); - $capacitor->charge(["name" => "third", "id" => "30"], $channel); + $capacitor->charge($channel, ["id" => 10, "name" => "first"]); + $capacitor->charge($channel, ["name" => "second", "id" => 20]); + $capacitor->charge($channel, ["name" => "third", "id" => "30"]); } function testChargeStrings() { @@ -45,8 +47,9 @@ class SqliteCapacitorTest extends TestCase { function testEach() { $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); - $capacitor->addChannel(new class extends CapacitorChannel { + $capacitor = new Capacitor($capacitor, new class extends CapacitorChannel { const NAME = "each"; + function getKeyDefinitions(): ?array { return [ "age" => "integer", @@ -60,12 +63,11 @@ class SqliteCapacitorTest extends TestCase { } }); - $channel = "each"; - $capacitor->reset($channel); - $capacitor->charge(["name" => "first", "age" => 5], $channel); - $capacitor->charge(["name" => "second", "age" => 10], $channel); - $capacitor->charge(["name" => "third", "age" => 15], $channel); - $capacitor->charge(["name" => "fourth", "age" => 20], $channel); + $capacitor->reset(); + $capacitor->charge(["name" => "first", "age" => 5]); + $capacitor->charge(["name" => "second", "age" => 10]); + $capacitor->charge(["name" => "third", "age" => 15]); + $capacitor->charge(["name" => "fourth", "age" => 20]); $setDone = function ($item, $row, $suffix=null) { $updates = ["done" => 1]; @@ -75,9 +77,39 @@ class SqliteCapacitorTest extends TestCase { } return $updates; }; - $capacitor->each(["age" => [">", 10]], $setDone, ["++"], $channel); - $capacitor->each(["done" => 0], $setDone, null, $channel); - Txx(iterator_to_array($capacitor->discharge(null, $channel, false))); + $capacitor->each(["age" => [">", 10]], $setDone, ["++"]); + $capacitor->each(["done" => 0], $setDone, null); + Txx(iterator_to_array($capacitor->discharge(null, false))); + + $capacitor->close(); + self::assertTrue(true); + } + + function testPrimayKey() { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($capacitor, new class extends CapacitorChannel { + const NAME = "pk"; + + function getKeyDefinitions(): ?array { + return [ + "_id" => "varchar primary key", + "done" => "integer default 0", + ]; + } + function getKeyValues($item): ?array { + return [ + "_id" => $item["numero"], + ]; + } + }); + + $capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]); + $capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]); + sleep(2); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]); $capacitor->close(); self::assertTrue(true);