modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2024-05-20 09:24:47 +04:00
parent c4e1496b82
commit a38fc17607
10 changed files with 359 additions and 105 deletions

45
src/db/Capacitor.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace nur\sery\db;
/**
* Class Capacitor: un objet permettant d'attaquer un canal spécique d'une
* instance de {@link ICapacitor}
*/
class Capacitor {
function __construct(ICapacitor $capacitor, CapacitorChannel $channel) {
$this->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();
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace nur\sery\db;
/**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
*/
class CapacitorChannel {
const NAME = null;
static function verifix_name(?string $name): string {
if ($name === null) $name = "default";
return strtolower($name);
}
function __construct(?string $name=null) {
$this->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;
}
}

39
src/db/ICapacitor.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace nur\sery\db;
/**
* Interface ICapacitor: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
interface ICapacitor {
/** tester si le canal spécifié existe */
function exists(?string $channel=null): bool;
/** supprimer le canal spécifié */
function reset(?string $channel=null): void;
/** charger une valeur dans le canal */
function charge($item, ?string $channel=null): void;
/** décharger les données du canal spécifié */
function discharge($keys=null, ?string $channel=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]
*/
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;
}

View File

@ -2,14 +2,15 @@
namespace nur\sery\db\sqlite; namespace nur\sery\db\sqlite;
use nur\sery\cl; use nur\sery\cl;
use nur\sery\db\CapacitorChannel;
use nur\sery\db\ICapacitor;
use nur\sery\php\func; use nur\sery\php\func;
use nur\sery\ValueException; use nur\sery\ValueException;
/** /**
* Class SqliteCapacitor: objet permettant d'accumuler des données pour les * Class SqliteCapacitor
* réutiliser plus tard
*/ */
class SqliteCapacitor { class SqliteCapacitor implements ICapacitor {
function __construct($sqlite) { function __construct($sqlite) {
$this->sqlite = Sqlite::with($sqlite); $this->sqlite = Sqlite::with($sqlite);
} }
@ -21,87 +22,114 @@ class SqliteCapacitor {
return $this->sqlite; return $this->sqlite;
} }
protected function getTableName(?string $channel): string { protected function _create(CapacitorChannel $channel): void {
return ($channel ?? "default")."_channel"; 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 */ /** tester si le canal spécifié existe */
function exists(?string $channel=null): bool { 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é */ /** supprimer le canal spécifié */
function reset(?string $channel=null) { function reset(?string $channel=null): void {
$tableName = $this->getTableName($channel); $this->_reset($this->channel($channel));
$this->sqlite->exec("drop table if exists $tableName");
#XXX maj de la tables channels dans une transaction
} }
/** @var array */ function _charge($item, CapacitorChannel $channel): void {
protected $created; $this->_create($channel);
$values = cl::merge($channel->getKeyValues($item), [
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), [
"_item" => serialize($item), "_item" => serialize($item),
]); ]);
$this->sqlite->exec([ $this->sqlite->exec([
"insert", "insert",
"into" => $this->getTableName($channel), "into" => $channel->getTableName(),
"values" => $values, "values" => $values,
]); ]);
} }
/** décharger les données du canal spécifié */ /** charger une valeur dans le canal */
function discharge($keys=null, ?string $channel=null, ?bool $reset=null): iterable { 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 ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys];
if ($reset === null) $reset = $keys === null; if ($reset === null) $reset = $keys === null;
$rows = $this->sqlite->all([ $rows = $this->sqlite->all([
"select _item", "select _item",
"from" => $this->getTableName($channel), "from" => $channel->getTableName(),
"where" => $keys, "where" => $keys,
]); ]);
foreach ($rows as $row) { foreach ($rows as $row) {
$item = unserialize($row['_item']); $item = unserialize($row['_item']);
yield $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] * si $keys n'est pas un tableau, il est transformé en ["_id" => $keys]
*/ */
function get($keys, ?string $channel=null) { function get($keys, ?string $channel=null) {
if ($keys === null) throw ValueException::null("keys"); return $this->_get($keys, $this->channel($channel));
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"]);
} }
/** function _each($keys, callable $func, ?array $args=null, CapacitorChannel $channel=null): 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 {
if ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys]; if ($keys !== null && !is_array($keys)) $keys = ["_id" => $keys];
$context = func::_prepare($func); $context = func::_prepare($func);
$sqlite = $this->sqlite; $sqlite = $this->sqlite;
$tableName = $this->getTableName($channel); $tableName = $channel->getTableName();
$rows = $sqlite->all([ $rows = $sqlite->all([
"select", "select",
"from" => $tableName, "from" => $tableName,
@ -147,20 +159,28 @@ class SqliteCapacitor {
if (array_key_exists("_item", $updates)) { if (array_key_exists("_item", $updates)) {
$updates["_item"] = serialize($updates["_item"]); $updates["_item"] = serialize($updates["_item"]);
} }
#XXXvvv migrer vers la syntaxe tableau de update $sqlite->exec([
$params = null; "update",
$sql = ["update", $tableName, "set"]; "table" => $tableName,
_query::parse_set_values($updates, $setsql, $params); "values" => $updates,
$sql[] = implode(", ", $setsql); "where" => ["_id" => $row["_id"]],
$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
} }
} }
} }
/**
* 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 { function close(): void {
$this->sqlite->close(); $this->sqlite->close();
} }

View File

@ -23,6 +23,8 @@ class _query {
$query = _query_update::parse($query, $params); $query = _query_update::parse($query, $params);
} elseif (_query_delete::isa($prefix)) { } elseif (_query_delete::isa($prefix)) {
$query = _query_delete::parse($query, $params); $query = _query_delete::parse($query, $params);
} elseif (_query_generic::isa($prefix)) {
$query = _query_generic::parse($query, $params);
} else { } else {
throw SqliteException::wrap(ValueException::invalid_kind($query, "query")); throw SqliteException::wrap(ValueException::invalid_kind($query, "query"));
} }

View File

@ -11,9 +11,37 @@ class _query_create extends _query {
]; ];
static function isa(string $sql): bool { 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 { 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);
} }
} }

View File

@ -10,9 +10,35 @@ class _query_delete extends _query {
]; ];
static function isa(string $sql): bool { 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 { 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);
} }
} }

View File

@ -0,0 +1,18 @@
<?php
namespace nur\sery\db\sqlite;
use nur\sery\cl;
use nur\sery\ValueException;
class _query_generic extends _query {
static function isa(string $sql): bool {
return preg_match('/^(?:drop\s+table)\b/i', $sql);
}
static function parse(array $query, ?array &$params=null): string {
if (!cl::is_list($query)) {
throw new ValueException("Seuls les tableaux séquentiels sont supportés");
}
return self::merge_seq($query);
}
}

View File

@ -13,9 +13,38 @@ class _query_update extends _query {
]; ];
static function isa(string $sql): bool { static function isa(string $sql): bool {
return false; return preg_match("/^update\b/i", $sql);
} }
static function parse(array $query, ?array &$params=null): string { 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"];
## set
_query::parse_set_values($query["values"], $setsql, $params);
$sql[] = "set";
$sql[] = implode(", ", $setsql);
## 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);
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace nur\sery\db\sqlite; namespace nur\sery\db\sqlite;
use nur\sery\db\CapacitorChannel;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class SqliteCapacitorTest extends TestCase { class SqliteCapacitorTest extends TestCase {
@ -26,20 +27,16 @@ class SqliteCapacitorTest extends TestCase {
} }
function testChargeArrays() { function testChargeArrays() {
$capacitor = new class(__DIR__.'/capacitor.db') extends SqliteCapacitor { $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
protected function getKeyDefinitions(?string $channel): ?array { $capacitor->addChannel(new class extends CapacitorChannel {
if ($channel === "arrays") { const NAME = "arrays";
return ["id" => "integer"]; function getKeyDefinitions(): ?array {
} return ["id" => "integer"];
return parent::getKeyDefinitions($channel);
} }
protected function getKeyValues($item, ?string $channel): ?array { function getKeyValues($item): ?array {
if ($channel === "arrays") { return ["id" => $item["id"] ?? null];
return ["id" => $item["id"] ?? null];
}
return parent::getKeyValues($item, $channel);
} }
}; });
$this->_testChargeStrings($capacitor, "strings"); $this->_testChargeStrings($capacitor, "strings");
$this->_testChargeArrays($capacitor, "arrays"); $this->_testChargeArrays($capacitor, "arrays");
@ -47,19 +44,21 @@ class SqliteCapacitorTest extends TestCase {
} }
function testEach() { function testEach() {
$capacitor = new class(__DIR__.'/capacitor.db') extends SqliteCapacitor { $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
protected function getKeyDefinitions(?string $channel): ?array { $capacitor->addChannel(new class extends CapacitorChannel {
const NAME = "each";
function getKeyDefinitions(): ?array {
return [ return [
"age" => "integer", "age" => "integer",
"done" => "integer default 0", "done" => "integer default 0",
]; ];
} }
protected function getKeyValues($item, ?string $channel): ?array { function getKeyValues($item): ?array {
return [ return [
"age" => $item["age"], "age" => $item["age"],
]; ];
} }
}; });
$channel = "each"; $channel = "each";
$capacitor->reset($channel); $capacitor->reset($channel);