diff --git a/src/db/Capacitor.php b/src/db/Capacitor.php new file mode 100644 index 0000000..d6c8d0f --- /dev/null +++ b/src/db/Capacitor.php @@ -0,0 +1,45 @@ +capacitor = $capacitor; + $this->channel = $channel; + } + + protected $capacitor; + + protected $channel; + + function exists(): bool { + return $this->capacitor->_exists($this->channel); + } + + function reset(): void { + $this->capacitor->_reset($this->channel); + } + + function charge($item) { + $this->capacitor->_charge($item, $this->channel); + } + + function discharge($keys=null, ?bool $reset=null): iterable { + return $this->capacitor->_discharge($keys, $this->channel, $reset); + } + + function get($keys) { + return $this->capacitor->_get($keys, $this->channel); + } + + function each($keys, callable $func, ?array $args=null): void { + $this->capacitor->_each($keys, $func, $args, $this->channel); + } + + function close(): void { + $this->capacitor->close(); + } +} diff --git a/src/db/CapacitorChannel.php b/src/db/CapacitorChannel.php new file mode 100644 index 0000000..2158120 --- /dev/null +++ b/src/db/CapacitorChannel.php @@ -0,0 +1,48 @@ +name = self::verifix_name($name ?? static::NAME); + $this->created = false; + } + + /** @var string */ + protected $name; + + function getName(): string { + return $this->name; + } + + function getTableName(): string { + return $this->name."_channel"; + } + + protected $created; + + function isCreated(): bool { + return $this->created; + } + + function setCreated(bool $created=true): void { + $this->created = $created; + } + + function getKeyDefinitions(): ?array { + return null; + } + + function getKeyValues($item): ?array { + return null; + } +} diff --git a/src/db/ICapacitor.php b/src/db/ICapacitor.php new file mode 100644 index 0000000..9ff0c4a --- /dev/null +++ b/src/db/ICapacitor.php @@ -0,0 +1,39 @@ + $keys] + */ + function get($keys, ?string $channel=null); + + /** + * 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; + + function close(): void; +} diff --git a/src/db/sqlite/SqliteCapacitor.php b/src/db/sqlite/SqliteCapacitor.php index 8cf075b..0ed1ae0 100644 --- a/src/db/sqlite/SqliteCapacitor.php +++ b/src/db/sqlite/SqliteCapacitor.php @@ -2,14 +2,15 @@ namespace nur\sery\db\sqlite; use nur\sery\cl; +use nur\sery\db\CapacitorChannel; +use nur\sery\db\ICapacitor; use nur\sery\php\func; use nur\sery\ValueException; /** - * Class SqliteCapacitor: objet permettant d'accumuler des données pour les - * réutiliser plus tard + * Class SqliteCapacitor */ -class SqliteCapacitor { +class SqliteCapacitor implements ICapacitor { function __construct($sqlite) { $this->sqlite = Sqlite::with($sqlite); } @@ -21,87 +22,114 @@ class SqliteCapacitor { return $this->sqlite; } - protected function getTableName(?string $channel): string { - return ($channel ?? "default")."_channel"; + protected function _create(CapacitorChannel $channel): void { + if (!$channel->isCreated()) { + $columns = cl::merge([ + "_id" => "integer primary key autoincrement", + "_item" => "text", + ], $channel->getKeyDefinitions()); + $this->sqlite->exec([ + "create table if not exists", + "table" => $channel->getTableName(), + "cols" => $columns, + ]); + $channel->setCreated(); + } + } + + /** @var CapacitorChannel[] */ + protected $channels; + + function addChannel(CapacitorChannel $channel): CapacitorChannel { + $this->_create($channel); + $this->channels[$channel->getName()] = $channel; + return $channel; + } + + protected function channel(?string $name=null): CapacitorChannel { + $name = CapacitorChannel::verifix_name($name); + $channel = $this->channels[$name] ?? null; + if ($channel === null) { + $channel = $this->addChannel(new CapacitorChannel($name)); + } + return $channel; + } + + function _exists(CapacitorChannel $channel): bool { + $tableName = $this->sqlite->get([ + "select name from sqlite_schema", + "where" => [ + "name" => $channel->getTableName(), + ], + ]); + return $tableName !== null; } /** tester si le canal spécifié existe */ function exists(?string $channel=null): bool { - #XXX maintenir une table channels avec la liste des canaux valides + return $this->_exists($this->channel($channel)); + } + + function _reset(CapacitorChannel $channel): void { + $this->sqlite->exec([ + "drop table if exists", + $channel->getTableName(), + ]); + $channel->setCreated(false); } /** supprimer le canal spécifié */ - function reset(?string $channel=null) { - $tableName = $this->getTableName($channel); - $this->sqlite->exec("drop table if exists $tableName"); - #XXX maj de la tables channels dans une transaction + function reset(?string $channel=null): void { + $this->_reset($this->channel($channel)); } - /** @var array */ - protected $created; - - protected function getKeyDefinitions(?string $channel): ?array { - return null; - } - - protected function getKeyValues($item, ?string $channel): ?array { - return null; - } - - protected function create(?string $channel): void { - if ($this->created[$channel] ?? false) return; - $columns = cl::merge([ - "_id" => "integer primary key autoincrement", - "_item" => "text", - ], $this->getKeyDefinitions($channel)); - #XXXvvv migrer vers la syntaxe tableau de create - $index = 0; - foreach ($columns as $column => &$definition) { - if ($column === $index) { - $index++; - } else { - $definition = "$column $definition"; - } - }; unset($definition); - $query = implode("", [ - "create table if not exists ", - $this->getTableName($channel), - " (", - implode(", ", $columns), - ")", - ]); - #XXX^^^ migrer vers la syntaxe tableau de create - $this->sqlite->exec($query); - #XXX maj de la tables channels dans une transaction - $this->created[$channel] = true; - } - - function charge($item, ?string $channel=null) { - $this->create($channel); - $values = cl::merge($this->getKeyValues($item, $channel), [ + function _charge($item, CapacitorChannel $channel): void { + $this->_create($channel); + $values = cl::merge($channel->getKeyValues($item), [ "_item" => serialize($item), ]); $this->sqlite->exec([ "insert", - "into" => $this->getTableName($channel), + "into" => $channel->getTableName(), "values" => $values, ]); } - /** décharger les données du canal spécifié */ - function discharge($keys=null, ?string $channel=null, ?bool $reset=null): iterable { + /** charger une valeur dans le canal */ + function charge($item, ?string $channel=null): void { + $this->_charge($item, $this->channel($channel)); + } + + 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; $rows = $this->sqlite->all([ "select _item", - "from" => $this->getTableName($channel), + "from" => $channel->getTableName(), "where" => $keys, ]); foreach ($rows as $row) { $item = unserialize($row['_item']); yield $item; } - if ($reset) $this->reset($channel); + if ($reset) $this->_reset($channel); + } + + /** 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->channel($channel), $reset); + } + + function _get($keys, CapacitorChannel $channel=null) { + if ($keys === null) throw ValueException::null("keys"); + if (!is_array($keys)) $keys = ["_id" => $keys]; + $row = $this->sqlite->one([ + "select _item", + "from" => $channel->getTableName(), + "where" => $keys, + ]); + if ($row === null) return null; + else return unserialize($row["_item"]); } /** @@ -110,30 +138,14 @@ class SqliteCapacitor { * si $keys n'est pas un tableau, il est transformé en ["_id" => $keys] */ function get($keys, ?string $channel=null) { - if ($keys === null) throw ValueException::null("keys"); - if (!is_array($keys)) $keys = ["_id" => $keys]; - $row = $this->sqlite->one([ - "select _item", - "from" => $this->getTableName($channel), - "where" => $keys, - ]); - if ($row === null) return null; - else return unserialize($row["_item"]); + return $this->_get($keys, $this->channel($channel)); } - /** - * 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 { + function _each($keys, callable $func, ?array $args=null, CapacitorChannel $channel=null): void { if ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys]; $context = func::_prepare($func); $sqlite = $this->sqlite; - $tableName = $this->getTableName($channel); + $tableName = $channel->getTableName(); $rows = $sqlite->all([ "select", "from" => $tableName, @@ -147,20 +159,28 @@ class SqliteCapacitor { if (array_key_exists("_item", $updates)) { $updates["_item"] = serialize($updates["_item"]); } - #XXXvvv migrer vers la syntaxe tableau de update - $params = null; - $sql = ["update", $tableName, "set"]; - _query::parse_set_values($updates, $setsql, $params); - $sql[] = implode(", ", $setsql); - $sql[] = "where"; - _query::parse_conds(["_id" => $row["_id"]], $wheresql, $params); - $sql[] = implode(" and ", $wheresql); - $sqlite->exec(implode(" ", $sql), $params); - #XXX^^^ migrer vers la syntaxe tableau de update + $sqlite->exec([ + "update", + "table" => $tableName, + "values" => $updates, + "where" => ["_id" => $row["_id"]], + ]); } } } + /** + * 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->channel($channel)); + } + function close(): void { $this->sqlite->close(); } diff --git a/src/db/sqlite/_query.php b/src/db/sqlite/_query.php index 110abfe..a89eca0 100644 --- a/src/db/sqlite/_query.php +++ b/src/db/sqlite/_query.php @@ -23,6 +23,8 @@ class _query { $query = _query_update::parse($query, $params); } elseif (_query_delete::isa($prefix)) { $query = _query_delete::parse($query, $params); + } elseif (_query_generic::isa($prefix)) { + $query = _query_generic::parse($query, $params); } else { throw SqliteException::wrap(ValueException::invalid_kind($query, "query")); } diff --git a/src/db/sqlite/_query_create.php b/src/db/sqlite/_query_create.php index 95b4b45..1dc9085 100644 --- a/src/db/sqlite/_query_create.php +++ b/src/db/sqlite/_query_create.php @@ -11,9 +11,37 @@ class _query_create extends _query { ]; static function isa(string $sql): bool { - return false; + //return preg_match("/^create(?:\s+table)?\b/i", $sql); + #XXX implémentation minimale + return preg_match("/^create\s+table\b/i", $sql); } static function parse(array $query, ?array &$params=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## columns + $cols = $query["cols"]; + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(".implode(", ", $cols).")"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); } } diff --git a/src/db/sqlite/_query_delete.php b/src/db/sqlite/_query_delete.php index 8599b71..7ff9fb2 100644 --- a/src/db/sqlite/_query_delete.php +++ b/src/db/sqlite/_query_delete.php @@ -10,9 +10,35 @@ class _query_delete extends _query { ]; static function isa(string $sql): bool { - return false; + //return preg_match("/^delete(?:\s+from)?\b/i", $sql); + #XXX implémentation minimale + return preg_match("/^delete\s+from\b/i", $sql); } static function parse(array $query, ?array &$params=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + _query::parse_conds($where, $wheresql, $params); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); } } diff --git a/src/db/sqlite/_query_generic.php b/src/db/sqlite/_query_generic.php new file mode 100644 index 0000000..0201150 --- /dev/null +++ b/src/db/sqlite/_query_generic.php @@ -0,0 +1,18 @@ + "integer"]; - } - return parent::getKeyDefinitions($channel); + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor->addChannel(new class extends CapacitorChannel { + const NAME = "arrays"; + function getKeyDefinitions(): ?array { + return ["id" => "integer"]; } - protected function getKeyValues($item, ?string $channel): ?array { - if ($channel === "arrays") { - return ["id" => $item["id"] ?? null]; - } - return parent::getKeyValues($item, $channel); + function getKeyValues($item): ?array { + return ["id" => $item["id"] ?? null]; } - }; + }); $this->_testChargeStrings($capacitor, "strings"); $this->_testChargeArrays($capacitor, "arrays"); @@ -47,19 +44,21 @@ class SqliteCapacitorTest extends TestCase { } function testEach() { - $capacitor = new class(__DIR__.'/capacitor.db') extends SqliteCapacitor { - protected function getKeyDefinitions(?string $channel): ?array { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor->addChannel(new class extends CapacitorChannel { + const NAME = "each"; + function getKeyDefinitions(): ?array { return [ "age" => "integer", "done" => "integer default 0", ]; } - protected function getKeyValues($item, ?string $channel): ?array { + function getKeyValues($item): ?array { return [ "age" => $item["age"], ]; } - }; + }); $channel = "each"; $capacitor->reset($channel);