Compare commits

..

5 Commits

Author SHA1 Message Date
5652d38073 ajout cl::split_assoc 2025-09-15 22:51:20 +04:00
96e046d5bf modifs.mineures sans commentaires 2025-09-15 17:30:25 +04:00
20c0cb2e0f modifs.mineures sans commentaires 2025-09-15 15:13:53 +04:00
2583ff70e5 modifs.mineures sans commentaires 2025-09-15 08:34:38 +04:00
1d9f9492a5 début 2025-09-13 10:39:44 +04:00
16 changed files with 1078 additions and 1173 deletions

View File

@ -17,6 +17,21 @@
</DockerContainerSettings> </DockerContainerSettings>
</value> </value>
</entry> </entry>
<entry key="38915385-b3ff-4f4b-8a9a-d5f3ecae559e">
<value>
<DockerContainerSettings>
<option name="version" value="1" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/opt/project" />
<option name="hostPath" value="$PROJECT_DIR$" />
</DockerVolumeBindingImpl>
</list>
</option>
</DockerContainerSettings>
</value>
</entry>
</map> </map>
</list> </list>
</component> </component>

80
.idea/php.xml generated
View File

@ -22,56 +22,56 @@
</component> </component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" /> <path value="$PROJECT_DIR$/php/vendor/composer" />
<path value="$PROJECT_DIR$/php/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/php/vendor/dflydev/dot-access-data" />
<path value="$PROJECT_DIR$/php/vendor/symfony/deprecation-contracts" /> <path value="$PROJECT_DIR$/php/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/php/vendor/symfony/yaml" /> <path value="$PROJECT_DIR$/php/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-text-template" /> <path value="$PROJECT_DIR$/php/vendor/league/config" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-file-iterator" /> <path value="$PROJECT_DIR$/php/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-timer" /> <path value="$PROJECT_DIR$/php/vendor/nette/schema" />
<path value="$PROJECT_DIR$/php/vendor/nette/utils" />
<path value="$PROJECT_DIR$/php/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/php/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/php/vendor/phpmailer/phpmailer" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-code-coverage" /> <path value="$PROJECT_DIR$/php/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/type" /> <path value="$PROJECT_DIR$/php/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-enumerator" /> <path value="$PROJECT_DIR$/php/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/version" /> <path value="$PROJECT_DIR$/php/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/global-state" /> <path value="$PROJECT_DIR$/php/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/php/vendor/psr/cache" />
<path value="$PROJECT_DIR$/php/vendor/psr/container" />
<path value="$PROJECT_DIR$/php/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/php/vendor/psr/log" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/complexity" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/environment" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/recursion-context" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/resource-operations" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/diff" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/cli-parser" /> <path value="$PROJECT_DIR$/php/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/php/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit-reverse-lookup" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/php/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/php/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/php/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/php/vendor/nulib/tests" />
<path value="$PROJECT_DIR$/php/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/php/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/php/vendor/composer" />
<path value="$PROJECT_DIR$/php/vendor/league/config" />
<path value="$PROJECT_DIR$/php/vendor/nette/schema" />
<path value="$PROJECT_DIR$/php/vendor/league/commonmark" />
<path value="$PROJECT_DIR$/php/vendor/dflydev/dot-access-data" />
<path value="$PROJECT_DIR$/php/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/php/vendor/nette/utils" />
<path value="$PROJECT_DIR$/php/vendor/psr/container" />
<path value="$PROJECT_DIR$/php/vendor/psr/log" />
<path value="$PROJECT_DIR$/php/vendor/psr/cache" />
<path value="$PROJECT_DIR$/php/vendor/phpmailer/phpmailer" />
<path value="$PROJECT_DIR$/php/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache" /> <path value="$PROJECT_DIR$/php/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/php/vendor/symfony/cache-contracts" /> <path value="$PROJECT_DIR$/php/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php80" /> <path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php80" />
<path value="$PROJECT_DIR$/php/vendor/symfony/service-contracts" /> <path value="$PROJECT_DIR$/php/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/php/vendor/symfony/polyfill-php73" />
<path value="$PROJECT_DIR$/php/vendor/symfony/var-exporter" /> <path value="$PROJECT_DIR$/php/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/php/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/php/vendor/theseer/tokenizer" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.4"> <component name="PhpProjectSharedConfiguration" php_language_level="7.4">

View File

@ -923,4 +923,48 @@ class cl {
A::usort($array, $keys, $assoc); A::usort($array, $keys, $assoc);
return $array; return $array;
} }
#############################################################################
/**
* Extraire d'un tableau les clés séquentielles et les clés associatives
*
* Retourner une liste [$list, $assoc] $list est un tableau avec uniquement
* les valeurs des clés séquentielles et $assoc est un tableau avec uniquement
* les valeurs des clés associatives. S'il n'existe aucune clé séquentielle
* (resp. aucune clé associative), $list (resp. $assoc) vaut null.
*
* Par exemple: split_assoc(["a", "b" => "c"]) retourne [["a"], ["b" => "c"]]
*/
static final function split_assoc(?array $array): array {
$list = null;
$assoc = null;
if ($array !== null) {
$i = 0;
foreach ($array as $key => $value) {
if ($key === $i) {
$list[] = $value;
$i++;
} else {
$assoc[$key] = $value;
}
}
}
return [$list, $assoc];
}
/**
* Joindre en un seul tableau un tableau avec des clés séquentielles et un
* tableau avec des clés associatives.
*
* Si $list_first==true, les clés séquentielles arrivent d'abord, ensuite les
* clés associatives. Sinon, ce sont les clés associatives qui arrivent d'abord
*/
static final function merge_assoc(?array &$array, ?array $list, ?array $assoc, bool $list_first=false): void {
if ($list === null && $assoc === null) $array = [];
elseif ($list === null) $array = $assoc;
elseif ($assoc === null) $array = $list;
elseif ($list_first) $array = array_merge($list, $assoc);
else $array = array_merge($assoc, $list);
}
} }

View File

@ -3,215 +3,728 @@ namespace nulib\db;
use nulib\A; use nulib\A;
use nulib\cl; use nulib\cl;
use nulib\cv;
use nulib\db\_private\_migration;
use nulib\php\func; use nulib\php\func;
use nulib\ValueException; use nulib\ValueException;
use Traversable; use Traversable;
/** /**
* Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une * Class Capacitor: objet permettant d'accumuler des données pour les
* instance de {@link CapacitorStorage} * réutiliser plus tard
*/ */
class Capacitor implements ITransactor { abstract class Capacitor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) { abstract function db(): IDatabase;
$this->storage = $storage;
$this->channel = $channel;
$this->channel->setCapacitor($this);
if ($ensureExists) $this->ensureExists();
}
/** @var CapacitorStorage */
protected $storage;
function getStorage(): CapacitorStorage {
return $this->storage;
}
function db(): IDatabase {
return $this->getStorage()->db();
}
function ensureLive(): self { function ensureLive(): self {
$this->getStorage()->ensureLive(); $this->db()->ensure();
return $this; return $this;
} }
/** @var CapacitorChannel */ function newChannel($channel): CapacitorChannel {
protected $channel; if (!($channel instanceof CapacitorChannel)) {
if (!is_array($channel)) $channel = ["name" => $channel];
function getChannel(): CapacitorChannel { $channel = new CapacitorChannel($channel);
return $this->channel;
}
function getTableName(): string {
return $this->getChannel()->getTableName();
}
function getCreateSql(): string {
$channel = $this->channel;
return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db());
}
/** @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) { return $channel->initCapacitor($this);
foreach ($channels as $channel) { }
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) { # les définitions sont par défaut pour MariaDB/MySQL
$this->subChannels[] = $channel; const CDATA_DEFINITION = "mediumtext";
} else { const CSUM_DEFINITION = "varchar(40)";
throw ValueException::invalid_type($channel, CapacitorChannel::class); const CTIMESTAMP_DEFINITION = "datetime";
} const GSERIAL_DEFINITION = "integer primary key auto_increment";
} const GLIC_DEFINITION = "varchar(80)";
const GLIB_DEFINITION = "varchar(255)";
const GTEXT_DEFINITION = "mediumtext";
const GBOOL_DEFINITION = "integer(1) default 0";
const GUUID_DEFINITION = "varchar(36)";
protected static function verifix_col($def): string {
if (!is_string($def)) $def = strval($def);
$def = trim($def);
$parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
$def = $parts[0];
$rest = " $parts[1]";
} else {
$rest = null;
} }
return $this; switch ($def) {
case "serdata":
case "Cdata": $def = static::CDATA_DEFINITION; break;
case "sersum":
case "Csum": $def = static::CSUM_DEFINITION; break;
case "serts":
case "Ctimestamp": $def = static::CTIMESTAMP_DEFINITION; break;
case "genserial":
case "Gserial": $def = static::GSERIAL_DEFINITION; break;
case "genlic":
case "Glic": $def = static::GLIC_DEFINITION; break;
case "genlib":
case "Glib": $def = static::GLIB_DEFINITION; break;
case "gentext":
case "Gtext": $def = static::GTEXT_DEFINITION; break;
case "genbool":
case "Gbool": $def = static::GBOOL_DEFINITION; break;
case "genuuid":
case "Guuid": $def = static::GUUID_DEFINITION; break;
}
return "$def$rest";
} }
function inTransaction(): bool { const PRIMARY_KEY_DEFINITION = [
return $this->db()->inTransaction(); "id_" => "Gserial",
} ];
function beginTransaction(?callable $func=null, bool $commit=true): void { const COLUMN_DEFINITIONS = [
$db = $this->db(); "item__" => "Cdata",
if ($this->subChannels !== null) { "item__sum_" => "Csum",
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait "created_" => "Ctimestamp",
if ($this->subManageTransactions === null) { "modified_" => "Ctimestamp",
foreach ($this->subChannels as $channel) { ];
$name = $channel->getName();
$this->subManageTransactions ??= []; protected function getColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
if (!array_key_exists($name, $this->subManageTransactions)) { $definitions = [];
$this->subManageTransactions[$name] = $channel->isManageTransactions(); if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::verifix_col($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
} }
$channel->setManageTransactions(false); } else {
$constraints[] = $def;
} }
if (!$db->inTransaction()) $db->beginTransaction(); } else {
$definitions[$col] = self::verifix_col($def);
} }
} elseif (!$db->inTransaction()) { }
return cl::merge($definitions, $constraints);
}
/** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $colDefs)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$colDefs = $this->getColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($colDefs) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
#############################################################################
# Migration et metadata
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db();
$db->exec([
"create table",
"table" => static::METADATA_TABLE,
"cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
protected function getCreateChannelSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->getColumnDefinitions($channel, true),
];
}
abstract function getMigration(CapacitorChannel $channel): _migration;
#############################################################################
# Catalogue
const CATALOG_TABLE = "_channels";
const CATALOG_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
protected function getCreateCatalogSql(): array {
return [
"create table if not exists",
"table" => static::CATALOG_TABLE,
"cols" => static::CATALOG_COLS,
];
}
protected function addToCatalogSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CATALOG_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
function getCatalog(): iterable {
return $this->db()->all([
"select",
"from" => static::CATALOG_TABLE,
]);
}
function isInCatalog(array $filter, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CATALOG_TABLE,
"where" => $filter,
]);
return $raw !== null;
}
#############################################################################
protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->getCreateCatalogSql());
$db->exec($this->addToCatalogSql($channel));
}
function create(CapacitorChannel $channel): void {
$this->prepareMetadata();
$this->getMigration($channel)->migrate($this->db());
$this->afterCreate($channel);
}
function autocreate(CapacitorChannel $channel, bool $force=false): void {
if ($force || !$channel->isCreated()) {
$channel->ensureSetup();
$this->create($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
protected function beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CATALOG_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->autocreate($channel);
}
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal
*/
function charge(CapacitorChannel $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
$channel->initCapacitor($this);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction(); $db->beginTransaction();
} }
if ($func !== null) { $nbModified = 0;
$commited = false; try {
try { if ($insert) {
func::call($func, $this); $id = $db->exec([
if ($commit) { "insert",
$this->commit(); "into" => $tableName,
$commited = true; "values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
} }
} finally {
if ($commit && !$commited) $this->rollback();
} }
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
} }
} }
protected function beforeEndTransaction(): void { /**
if ($this->subManageTransactions !== null) { * décharger les données du canal spécifié. seul la valeur de $item est
foreach ($this->subChannels as $channel) { * fournie
$name = $channel->getName(); */
$channel->setManageTransactions($this->subManageTransactions[$name]); function discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$channel->initCapacitor($this);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->reset($channel);
}
protected function convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
} }
$this->subManageTransactions = null; }
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->getColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->convertValue2row($channel, $filter, $cols);
} }
} }
function commit(): void { /** indiquer le nombre d'éléments du canal spécifié */
$this->beforeEndTransaction(); function count(CapacitorChannel $channel, $filter): int {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$channel->initCapacitor($this);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
/**
* appeler une fonction pour chaque élément du canal spécifié.
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function each(CapacitorChannel $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db(); $db = $this->db();
if ($db->inTransaction()) $db->commit(); # si on est déjà dans une transaction, désactiver la gestion des transactions
} $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
function rollback(): void { $commited = false;
$this->beforeEndTransaction(); $db->beginTransaction();
$db = $this->db(); $commitThreshold = $channel->getEachCommitThreshold();
if ($db->inTransaction()) $db->rollback(); }
}
function exists(): bool {
return $this->storage->_exists($this->channel);
}
function ensureExists(): void {
$this->storage->_ensureExists($this->channel);
}
function reset(bool $recreate=false): void {
$this->storage->_reset($this->channel, $recreate);
}
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $row);
}
function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
$count = 0; $count = 0;
if ($items !== null) { $nbUpdated = 0;
if ($func !== null) { $tableName = $channel->getTableName();
$func = func::with($func, $args)->bind($this->channel); try {
$args ??= [];
$rows = $this->all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
} }
foreach ($items as $item) { if ($manageTransactions) {
$count += $this->charge($item, $func); $db->commit();
$commited = true;
} }
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
} }
return $count;
} }
function discharge(bool $reset=true): Traversable { /**
return $this->storage->_discharge($this->channel, $reset); * supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function delete(CapacitorChannel $channel, $filter, $func=null, ?array $args=null): int {
$channel->initCapacitor($this);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
} }
function count($filter=null): int { abstract function close(): void;
return $this->storage->_count($this->channel, $filter);
}
function one($filter, ?array $mergeQuery=null): ?array {
return $this->storage->_one($this->channel, $filter, $mergeQuery);
}
function all($filter, ?array $mergeQuery=null): Traversable {
return $this->storage->_all($this->channel, $filter, $mergeQuery);
}
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args, $mergeQuery, $nbUpdated);
}
function delete($filter, $func=null, ?array $args=null): int {
if ($this->subChannels !== null) $this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args);
}
function dbAll(array $query, ?array $params=null): iterable {
$primaryKeys = $this->channel->getPrimaryKeys();
return $this->storage->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
}
function dbOne(array $query, ?array $params=null): ?array {
return $this->storage->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
}
/** @return int|false */
function dbUpdate(array $query, ?array $params=null) {
return $this->storage->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
}
function close(): void {
$this->storage->close();
}
} }

View File

@ -1,18 +1,23 @@
<?php <?php
namespace nulib\db; namespace nulib\db;
use nulib\app;
use nulib\cl; use nulib\cl;
use nulib\php\func;
use nulib\str; use nulib\str;
use nulib\ValueException;
use Traversable; use Traversable;
/** /**
* Class CapacitorChannel: un canal d'une instance de {@link ICapacitor} * Class CapacitorChannel: un canal de données
*/ */
class CapacitorChannel implements ITransactor { class CapacitorChannel implements ITransactor {
const NAME = null; const NAME = null;
const TABLE_NAME = null; const TABLE_NAME = null;
const AUTOCREATE = null;
protected function COLUMN_DEFINITIONS(): ?array { protected function COLUMN_DEFINITIONS(): ?array {
return static::COLUMN_DEFINITIONS; return static::COLUMN_DEFINITIONS;
} const COLUMN_DEFINITIONS = null; } const COLUMN_DEFINITIONS = null;
@ -50,16 +55,21 @@ class CapacitorChannel implements ITransactor {
return $eachCommitThreshold; return $eachCommitThreshold;
} }
function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) { function __construct(?array $params=null) {
$name ??= static::NAME; $this->capacitor = null;
$tableName ??= static::TABLE_NAME;
$name = $params["name"] ?? static::NAME;
$tableName = $params["tableName"] ?? static::TABLE_NAME;
self::verifix_name($name, $tableName); self::verifix_name($name, $tableName);
$this->name = $name; $this->name = $name;
$this->tableName = $tableName; $this->tableName = $tableName;
$this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold); #$autocreate = $params["autocreate"] ?? null;
#$autocreate ??= !app::get()->isProductionMode();
$autocreate = true; #XXX
$this->created = !$autocreate;
$this->setup = false; $this->setup = false;
$this->created = false;
$columnDefinitions = $this->COLUMN_DEFINITIONS(); $columnDefinitions = $this->COLUMN_DEFINITIONS();
$primaryKeys = cl::withn(static::PRIMARY_KEYS); $primaryKeys = cl::withn(static::PRIMARY_KEYS);
$migration = cl::withn(static::MIGRATION); $migration = cl::withn(static::MIGRATION);
@ -117,6 +127,13 @@ class CapacitorChannel implements ITransactor {
$this->columnDefinitions = $columnDefinitions; $this->columnDefinitions = $columnDefinitions;
$this->primaryKeys = $primaryKeys; $this->primaryKeys = $primaryKeys;
$this->migration = $migration; $this->migration = $migration;
$manageTransactions = $params["manageTransactions"] ?? static::MANAGE_TRANSACTIONS;
$this->manageTransactions = $manageTransactions;
$eachCommitThreshold = $params["eachCommitThreshold"] ?? null;
$eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
$this->eachCommitThreshold = $eachCommitThreshold;
} }
protected string $name; protected string $name;
@ -131,40 +148,6 @@ class CapacitorChannel implements ITransactor {
return $this->tableName; return $this->tableName;
} }
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/** /**
* initialiser ce channel avant sa première utilisation. * initialiser ce channel avant sa première utilisation.
*/ */
@ -319,6 +302,40 @@ class CapacitorChannel implements ITransactor {
return $sum !== $psum; return $sum !== $psum;
} }
/**
* @var bool indiquer si les modifications de each doivent être gérées dans
* une transaction. si false, l'utilisateur doit lui même gérer la
* transaction.
*/
protected bool $manageTransactions;
function isManageTransactions(): bool {
return $this->manageTransactions;
}
function setManageTransactions(bool $manageTransactions=true): self {
$this->manageTransactions = $manageTransactions;
return $this;
}
/**
* @var ?int nombre maximum de modifications dans une transaction avant un
* commit automatique dans {@link Capacitor::each()}. Utiliser null pour
* désactiver la fonctionnalité.
*
* ce paramètre n'a d'effet que si $manageTransactions==true
*/
protected ?int $eachCommitThreshold;
function getEachCommitThreshold(): ?int {
return $this->eachCommitThreshold;
}
function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
$this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
return $this;
}
/** /**
* méthode appelée lors du chargement avec {@link Capacitor::charge()} pour * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
* créer un nouvel élément * créer un nouvel élément
@ -400,24 +417,20 @@ class CapacitorChannel implements ITransactor {
############################################################################# #############################################################################
# Méthodes déléguées pour des workflows centrés sur le channel # Méthodes déléguées pour des workflows centrés sur le channel
/**
* @var Capacitor|null instance de Capacitor par laquelle cette instance est
* utilisée
*/
protected ?Capacitor $capacitor; protected ?Capacitor $capacitor;
function getCapacitor(): ?Capacitor { function getCapacitor(): ?Capacitor {
return $this->capacitor; return $this->capacitor;
} }
function setCapacitor(Capacitor $capacitor): self { function initCapacitor(Capacitor $capacitor, bool $autocreate=true): self {
$this->capacitor = $capacitor; if ($this->capacitor === null) $this->capacitor = $capacitor;
if ($autocreate) $this->capacitor->autocreate($this);
return $this; return $this;
} }
function initStorage(CapacitorStorage $storage): self { function db(): IDatabase {
new Capacitor($storage, $this); return $this->capacitor->db();
return $this;
} }
function ensureLive(): self { function ensureLive(): self {
@ -425,52 +438,117 @@ class CapacitorChannel implements ITransactor {
return $this; return $this;
} }
function willUpdate(...$transactors): ITransactor { function getCreateSql(): string {
return $this->capacitor->willUpdate(...$transactors); return $this->capacitor->getMigration($this)->getSql(get_class($this), $this->db());
}
/** @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;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw ValueException::invalid_type($channel, CapacitorChannel::class);
}
}
}
return $this;
} }
function inTransaction(): bool { function inTransaction(): bool {
return $this->capacitor->inTransaction(); return $this->db()->inTransaction();
} }
function beginTransaction(?callable $func=null, bool $commit=true): void { function beginTransaction(?callable $func=null, bool $commit=true): void {
$this->capacitor->beginTransaction($func, $commit); $db = $this->db();
if ($this->subChannels !== null) {
# on gère des subchannels: ne débuter la transaction que si ce n'est déjà fait
if ($this->subManageTransactions === 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);
}
if (!$db->inTransaction()) $db->beginTransaction();
}
} elseif (!$db->inTransaction()) {
$db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
if ($commit) {
$this->commit();
$commited = true;
}
} finally {
if ($commit && !$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 { function commit(): void {
$this->capacitor->commit(); $this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $db->commit();
} }
function rollback(): void { function rollback(): void {
$this->capacitor->rollback(); $this->beforeEndTransaction();
} $db = $this->db();
if ($db->inTransaction()) $db->rollback();
function db(): IDatabase {
return $this->capacitor->getStorage()->db();
} }
function exists(): bool { function exists(): bool {
return $this->capacitor->exists(); return $this->capacitor->exists($this);
}
function ensureExists(): void {
$this->capacitor->ensureExists();
} }
function reset(bool $recreate=false): void { function reset(bool $recreate=false): void {
$this->capacitor->reset($recreate); $this->capacitor->reset($this, $recreate);
} }
function charge($item, $func=null, ?array $args=null, ?array &$row=null): int { function charge($item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->capacitor->charge($item, $func, $args, $row); return $this->capacitor->charge($this, $item, $func, $args, $row);
} }
function chargeAll(?iterable $items, $func=null, ?array $args=null): int { function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
return $this->capacitor->chargeAll($items, $func, $args); $count = 0;
if ($items !== null) {
if ($func !== null) {
$func = func::with($func, $args)->bind($this);
}
foreach ($items as $item) {
$count += $this->charge($item, $func);
}
}
return $count;
} }
function discharge(bool $reset=true): Traversable { function discharge(bool $reset=true): Traversable {
return $this->capacitor->discharge($reset); return $this->capacitor->discharge($this, $reset);
} }
/** /**
@ -496,40 +574,50 @@ class CapacitorChannel implements ITransactor {
function count($filter=null): int { function count($filter=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->count($filter); return $this->capacitor->count($this, $filter);
} }
function one($filter, ?array $mergeQuery=null): ?array { function one($filter, ?array $mergeQuery=null): ?array {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->one($filter, $mergeQuery); return $this->capacitor->one($this, $filter, $mergeQuery);
} }
function all($filter, ?array $mergeQuery=null): Traversable { function all($filter, ?array $mergeQuery=null): Traversable {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->all($filter, $mergeQuery); return $this->capacitor->all($this, $filter, $mergeQuery);
} }
function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated); return $this->capacitor->each($this, $filter, $func, $args, $mergeQuery, $nbUpdated);
} }
function delete($filter, $func=null, ?array $args=null): int { function delete($filter, $func=null, ?array $args=null): int {
$this->verifixFilter($filter); $this->verifixFilter($filter);
return $this->capacitor->delete($filter, $func, $args); return $this->capacitor->delete($this, $filter, $func, $args);
} }
function dbAll(array $query, ?array $params=null): iterable { function dbAll(array $query, ?array $params=null): iterable {
return $this->capacitor->dbAll($query, $params); $primaryKeys = $this->getPrimaryKeys();
return $this->capacitor->db()->all(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params, $primaryKeys);
} }
function dbOne(array $query, ?array $params=null): ?array { function dbOne(array $query, ?array $params=null): ?array {
return $this->capacitor->dbOne($query, $params); return $this->capacitor->db()->one(cl::merge([
"select",
"from" => $this->getTableName(),
], $query), $params);
} }
/** @return int|false */ /** @return int|false */
function dbUpdate(array $query, ?array $params=null) { function dbUpdate(array $query, ?array $params=null) {
return $this->capacitor->dbUpdate($query, $params); return $this->capacitor->db()->exec(cl::merge([
"update",
"table" => $this->getTableName(),
], $query), $params);
} }
function close(): void { function close(): void {

View File

@ -1,770 +0,0 @@
<?php
namespace nulib\db;
use nulib\A;
use nulib\cl;
use nulib\cv;
use nulib\db\_private\_migration;
use nulib\php\func;
use nulib\ValueException;
use Traversable;
/**
* Class CapacitorStorage: objet permettant d'accumuler des données pour les
* réutiliser plus tard
*/
abstract class CapacitorStorage {
abstract function db(): IDatabase;
function ensureLive(): self {
$this->db()->ensure();
return $this;
}
/** @var CapacitorChannel[] */
protected $channels;
function addChannel(CapacitorChannel $channel): CapacitorChannel {
$this->_create($channel);
$this->channels[$channel->getName()] = $channel;
return $channel;
}
protected function getChannel(?string $name): CapacitorChannel {
CapacitorChannel::verifix_name($name);
$channel = $this->channels[$name] ?? null;
if ($channel === null) {
$channel = $this->addChannel(new CapacitorChannel($name));
}
return $channel;
}
const PRIMARY_KEY_DEFINITION = [
"id_" => "genserial",
];
# les définitions sont par défaut pour MariaDB/MySQL
const SERDATA_DEFINITION = "mediumtext";
const SERSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "datetime";
const GENSERIAL_DEFINITION = "integer primary key auto_increment";
const GENLIC_DEFINITION = "varchar(80)";
const GENLIB_DEFINITION = "varchar(255)";
const GENTEXT_DEFINITION = "mediumtext";
const GENBOOL_DEFINITION = "integer(1) default 0";
const GENUUID_DEFINITION = "varchar(36)";
protected static function gencol($def): string {
if (!is_string($def)) $def = strval($def);
$def = trim($def);
$parts = preg_split('/\s+/', $def, 2);
if (count($parts) == 2) {
$def = $parts[0];
$rest = " $parts[1]";
} else {
$rest = null;
}
switch ($def) {
case "serdata": $def = static::SERDATA_DEFINITION; break;
case "sersum": $def = static::SERSUM_DEFINITION; break;
case "serts": $def = static::SERTS_DEFINITION; break;
case "genserial": $def = static::GENSERIAL_DEFINITION; break;
case "genlic": $def = static::GENLIC_DEFINITION; break;
case "genlib": $def = static::GENLIB_DEFINITION; break;
case "gentext": $def = static::GENTEXT_DEFINITION; break;
case "genbool": $def = static::GENBOOL_DEFINITION; break;
case "genuuid": $def = static::GENUUID_DEFINITION; break;
}
return "$def$rest";
}
const COLUMN_DEFINITIONS = [
"item__" => "serdata",
"item__sum_" => "sersum",
"created_" => "serts",
"modified_" => "serts",
];
protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array {
$definitions = [];
if ($channel->getPrimaryKeys() === null) {
$definitions[] = static::PRIMARY_KEY_DEFINITION;
}
$definitions[] = $channel->getColumnDefinitions();
$definitions[] = static::COLUMN_DEFINITIONS;
# forcer les définitions sans clé à la fin (sqlite requière par exemple que
# primary key (columns) soit à la fin)
$tmp = cl::merge(...$definitions);
$definitions = [];
$constraints = [];
$index = 0;
foreach ($tmp as $col => $def) {
if ($col === $index) {
$index++;
if (is_array($def)) {
if (!$ignoreMigrations) {
$mdefs = $def;
$mindex = 0;
foreach ($mdefs as $mcol => $mdef) {
if ($mcol === $mindex) {
$mindex++;
} else {
if ($mdef) {
$definitions[$mcol] = self::gencol($mdef);
} else {
unset($definitions[$mcol]);
}
}
}
}
} else {
$constraints[] = $def;
}
} else {
$definitions[$col] = self::gencol($def);
}
}
return cl::merge($definitions, $constraints);
}
protected function getMigration(CapacitorChannel $channel): ?array {
return $channel->getMigration($this->db()->getPrefix());
}
/** sérialiser les valeurs qui doivent l'être dans $row */
protected function serialize(CapacitorChannel $channel, ?array $row): ?array {
if ($row === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$raw = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif ($channel->isSerialCol($key)) {
[$serialCol, $sumCol] = $channel->getSumCols($key);
if (array_key_exists($key, $row)) {
$sum = $channel->getSum($key, $row[$key]);
$raw[$serialCol] = $sum[$serialCol];
if (array_key_exists($sumCol, $cols)) {
$raw[$sumCol] = $sum[$sumCol];
}
}
} elseif (array_key_exists($key, $row)) {
$raw[$col] = $row[$key];
}
}
return $raw;
}
/** désérialiser les valeurs qui doivent l'être dans $values */
protected function unserialize(CapacitorChannel $channel, ?array $raw): ?array {
if ($raw === null) return null;
$cols = $this->ColumnDefinitions($channel);
$index = 0;
$row = [];
foreach (array_keys($cols) as $col) {
$key = $col;
if ($key === $index) {
$index++;
} elseif (!array_key_exists($col, $raw)) {
} elseif ($channel->isSerialCol($key)) {
$value = $raw[$col];
if ($value !== null) $value = $channel->unserialize($value);
$row[$key] = $value;
} else {
$row[$key] = $raw[$col];
}
}
return $row;
}
function getPrimaryKeys(CapacitorChannel $channel): array {
$primaryKeys = $channel->getPrimaryKeys();
if ($primaryKeys === null) $primaryKeys = ["id_"];
return $primaryKeys;
}
function getRowIds(CapacitorChannel $channel, ?array $row, ?array &$primaryKeys=null): ?array {
$primaryKeys = $this->getPrimaryKeys($channel);
$rowIds = cl::select($row, $primaryKeys);
if (cl::all_n($rowIds)) return null;
else return $rowIds;
}
protected function _createSql(CapacitorChannel $channel): array {
return [
"create table if not exists",
"table" => $channel->getTableName(),
"cols" => $this->ColumnDefinitions($channel, true),
];
}
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
const METADATA_COLS = [
"name" => "varchar not null primary key",
"value" => "varchar",
];
protected function _prepareMetadata(): void {
if (!$this->tableExists(static::METADATA_TABLE)) {
$db = $this->db();
$db->exec([
"drop table if exists",
"table" => self::CHANNELS_TABLE,
]);
$db->exec([
"drop table if exists",
"table" => _migration::MIGRATION_TABLE,
]);
$db->exec([
"create table",
"table" => static::METADATA_TABLE,
"cols" => static::METADATA_COLS,
]);
$db->exec([
"insert",
"into" => static::METADATA_TABLE,
"values" => [
"name" => "version",
"value" => "1",
],
]);
}
}
abstract function _getMigration(CapacitorChannel $channel): _migration;
const CHANNELS_TABLE = "_channels";
const CHANNELS_COLS = [
"name" => "varchar not null primary key",
"table_name" => "varchar",
"class_name" => "varchar",
];
function channelExists(string $name, ?array &$raw=null): bool {
$raw = $this->db()->one([
"select",
"from" => static::CHANNELS_TABLE,
"where" => ["name" => $name],
]);
return $raw !== null;
}
function getChannels(): iterable {
return $this->db()->all([
"select",
"from" => static::CHANNELS_TABLE,
]);
}
protected function _createChannelsSql(): array {
return [
"create table if not exists",
"table" => static::CHANNELS_TABLE,
"cols" => static::CHANNELS_COLS,
];
}
protected function _addToChannelsSql(CapacitorChannel $channel): array {
return [
"insert",
"into" => static::CHANNELS_TABLE,
"values" => [
"name" => $channel->getName(),
"table_name" => $channel->getTableName(),
"class_name" => get_class($channel),
],
];
}
protected function _afterCreate(CapacitorChannel $channel): void {
$db = $this->db();
$db->exec($this->_createChannelsSql());
$db->exec($this->_addToChannelsSql($channel));
}
protected function _create(CapacitorChannel $channel): void {
$channel->ensureSetup();
if (!$channel->isCreated()) {
$this->_prepareMetadata();
$this->_getMigration($channel)->migrate($this->db());
$this->_afterCreate($channel);
$channel->setCreated();
}
}
/** tester si le canal spécifié existe */
function _exists(CapacitorChannel $channel): bool {
return $this->tableExists($channel->getTableName());
}
function exists(?string $channel): bool {
return $this->_exists($this->getChannel($channel));
}
/** s'assurer que le canal spécifié existe */
function _ensureExists(CapacitorChannel $channel): void {
$this->_create($channel);
}
function ensureExists(?string $channel): void {
$this->_ensureExists($this->getChannel($channel));
}
protected function _beforeReset(CapacitorChannel $channel): void {
$db = $this->db;
$name = $channel->getName();
$db->exec([
"delete",
"from" => _migration::MIGRATION_TABLE,
"where" => [
"channel" => $name,
],
]);
$db->exec([
"delete",
"from" => static::CHANNELS_TABLE,
"where" => [
"name" => $name,
],
]);
}
/** supprimer le canal spécifié */
function _reset(CapacitorChannel $channel, bool $recreate=false): void {
$this->_beforeReset($channel);
$this->db()->exec([
"drop table if exists",
$channel->getTableName(),
]);
$channel->setCreated(false);
if ($recreate) $this->_ensureExists($channel);
}
function reset(?string $channel, bool $recreate=false): void {
$this->_reset($this->getChannel($channel), $recreate);
}
/**
* charger une valeur dans le canal
*
* Après avoir calculé les valeurs des clés supplémentaires
* avec {@link CapacitorChannel::getItemValues()}, l'une des deux fonctions
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* est appelée en fonction du type d'opération: création ou mise à jour
*
* Ensuite, si $func !== null, la fonction est appelée avec la signature de
* {@link CapacitorChannel::onCreate()} ou {@link CapacitorChannel::onUpdate()}
* en fonction du type d'opération: création ou mise à jour
*
* Dans les deux cas, si la fonction retourne un tableau, il est utilisé pour
* modifier les valeurs insérées/mises à jour. De plus, $row obtient la
* valeur finale des données insérées/mises à jour
*
* Si $args est renseigné, il est ajouté aux arguments utilisés pour appeler
* les méthodes {@link CapacitorChannel::getItemValues()},
* {@link CapacitorChannel::onCreate()} et/ou
* {@link CapacitorChannel::onUpdate()}
*
* @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait
* déjà à l'identique dans le canal
*/
function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$row=null): int {
$this->_create($channel);
$tableName = $channel->getTableName();
$db = $this->db();
$args ??= [];
$row = func::call([$channel, "getItemValues"], $item, ...$args);
if ($row === [false]) return 0;
if ($row !== null && array_key_exists("item", $row)) {
$item = A::pop($row, "item");
}
$raw = cl::merge(
$channel->getSum("item", $item),
$this->serialize($channel, $row));
$praw = null;
$rowIds = $this->getRowIds($channel, $raw, $primaryKeys);
if ($rowIds !== null) {
# modification
$praw = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
]);
}
$now = date("Y-m-d H:i:s");
$insert = null;
if ($praw === null) {
# création
$raw = cl::merge($raw, [
"created_" => $now,
"modified_" => $now,
]);
$insert = true;
$initFunc = func::with([$channel, "onCreate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = null;
} else {
# modification
# intégrer autant que possible les valeurs de praw dans raw, de façon que
# l'utilisateur puisse voir clairement ce qui a été modifié
if ($channel->_wasSumModified("item", $raw, $praw)) {
$insert = false;
$raw = cl::merge($praw, $raw, [
"modified_" => $now,
]);
} else {
$raw = cl::merge($praw, $raw);
}
$initFunc = func::with([$channel, "onUpdate"], $args);
$row = $this->unserialize($channel, $raw);
$prow = $this->unserialize($channel, $praw);
}
$updates = $initFunc->prependArgs([$item, $row, $prow])->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
if ($func !== null) {
$updates = func::with($func, $args)
->prependArgs([$item, $row, $prow])
->bind($channel)
->invoke();
if ($updates === [false]) return 0;
if (is_array($updates) && $updates) {
if ($insert === null) $insert = false;
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = $now;
}
$row = cl::merge($row, $updates);
$raw = cl::merge($raw, $this->serialize($channel, $updates));
}
}
# aucune modification
if ($insert === null) return 0;
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
$nbModified = 0;
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $raw,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$row[$primaryKeys[0]] = $id;
}
$nbModified = 1;
} else {
# calculer ce qui a changé pour ne mettre à jour que le nécessaire
$updates = [];
foreach ($raw as $col => $value) {
if (array_key_exists($col, $rowIds)) {
# ne jamais mettre à jour la clé primaire
continue;
}
if (!cv::equals($value, $praw[$col] ?? null)) {
$updates[$col] = $value;
}
}
if (count($updates) == 1 && array_key_first($updates) == "modified_") {
# si l'unique modification porte sur la date de modification, alors
# la ligne n'est pas modifiée. ce cas se présente quand on altère la
# valeur de $item
$updates = null;
}
if ($updates) {
$db->exec([
"update",
"table" => $tableName,
"values" => $updates,
"where" => $rowIds,
]);
$nbModified = 1;
}
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $nbModified;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$row=null): int {
return $this->_charge($this->getChannel($channel), $item, $func, $args, $row);
}
/**
* décharger les données du canal spécifié. seul la valeur de $item est
* fournie
*/
function _discharge(CapacitorChannel $channel, bool $reset=true): Traversable {
$this->_create($channel);
$raws = $this->db()->all([
"select item__",
"from" => $channel->getTableName(),
]);
foreach ($raws as $raw) {
yield unserialize($raw['item__']);
}
if ($reset) $this->_reset($channel);
}
function discharge(?string $channel, bool $reset=true): Traversable {
return $this->_discharge($this->getChannel($channel), $reset);
}
protected function _convertValue2row(CapacitorChannel $channel, array $filter, array $cols): array {
$index = 0;
$fixed = [];
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (is_array($value)) {
$value = $this->_convertValue2row($channel, $value, $cols);
}
$fixed[] = $value;
} else {
$col = "${key}__";
if (array_key_exists($col, $cols)) {
# colonne sérialisée
$fixed[$col] = $channel->serialize($value);
} else {
$fixed[$key] = $value;
}
}
}
return $fixed;
}
protected function verifixFilter(CapacitorChannel $channel, &$filter): void {
if ($filter !== null && !is_array($filter)) {
$primaryKeys = $this->getPrimaryKeys($channel);
$id = $filter;
$channel->verifixId($id);
$filter = [$primaryKeys[0] => $id];
}
$cols = $this->ColumnDefinitions($channel);
if ($filter !== null) {
$filter = $this->_convertValue2row($channel, $filter, $cols);
}
}
/** indiquer le nombre d'éléments du canal spécifié */
function _count(CapacitorChannel $channel, $filter): int {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
return $this->db()->get([
"select count(*)",
"from" => $channel->getTableName(),
"where" => $filter,
]);
}
function count(?string $channel, $filter=null): int {
return $this->_count($this->getChannel($channel), $filter);
}
/**
* obtenir la ligne correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _one(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): ?array {
if ($filter === null) throw ValueException::null("filter");
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raw = $this->db()->one(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery));
return $this->unserialize($channel, $raw);
}
function one(?string $channel, $filter, ?array $mergeQuery=null): ?array {
return $this->_one($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* obtenir les lignes correspondant au filtre sur le canal spécifié
*
* si $filter n'est pas un tableau, il est transformé en ["id_" => $filter]
*/
function _all(CapacitorChannel $channel, $filter, ?array $mergeQuery=null): Traversable {
$this->_create($channel);
$this->verifixFilter($channel, $filter);
$raws = $this->db()->all(cl::merge([
"select",
"from" => $channel->getTableName(),
"where" => $filter,
], $mergeQuery), null, $this->getPrimaryKeys($channel));
foreach ($raws as $key => $raw) {
yield $key => $this->unserialize($channel, $raw);
}
}
function all(?string $channel, $filter, $mergeQuery=null): Traversable {
return $this->_all($this->getChannel($channel), $filter, $mergeQuery);
}
/**
* appeler une fonction pour chaque élément du canal spécifié.
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onEach()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @param int $nbUpdated reçoit le nombre de lignes mises à jour
* @return int le nombre de lignes parcourues
*/
function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onEach;
$onEach = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$nbUpdated = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter, $mergeQuery);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$updates = $onEach->invoke([$row, ...$args]);
if ($updates === [false]) {
break;
} elseif ($updates !== null) {
if (!array_key_exists("modified_", $updates)) {
$updates["modified_"] = date("Y-m-d H:i:s");
}
$nbUpdated += $db->exec([
"update",
"table" => $tableName,
"values" => $this->serialize($channel, $updates),
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function each(?string $channel, $filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
return $this->_each($this->getChannel($channel), $filter, $func, $args, $mergeQuery, $nbUpdated);
}
/**
* supprimer tous les éléments correspondant au filtre et pour lesquels la
* fonction retourne une valeur vraie si elle est spécifiée
*
* $filter permet de filtrer parmi les élements chargés
*
* $func est appelé avec la signature de {@link CapacitorChannel::onDelete()}
* si la fonction retourne un tableau, il est utilisé pour mettre à jour la
* ligne
*
* @return int le nombre de lignes parcourues
*/
function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int {
$this->_create($channel);
if ($func === null) $func = CapacitorChannel::onDelete;
$onDelete = func::with($func)->bind($channel);
$db = $this->db();
# si on est déjà dans une transaction, désactiver la gestion des transactions
$manageTransactions = $channel->isManageTransactions() && !$db->inTransaction();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
$count = 0;
$tableName = $channel->getTableName();
try {
$args ??= [];
$rows = $this->_all($channel, $filter);
foreach ($rows as $row) {
$rowIds = $this->getRowIds($channel, $row);
$shouldDelete = boolval($onDelete->invoke([$row, ...$args]));
if ($shouldDelete) {
$db->exec([
"delete",
"from" => $tableName,
"where" => $rowIds,
]);
if ($manageTransactions && $commitThreshold !== null) {
$commitThreshold--;
if ($commitThreshold <= 0) {
$db->commit();
$db->beginTransaction();
$commitThreshold = $channel->getEachCommitThreshold();
}
}
}
$count++;
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return $count;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
}
function delete(?string $channel, $filter, $func=null, ?array $args=null): int {
return $this->_delete($this->getChannel($channel), $filter, $func, $args);
}
abstract function close(): void;
}

View File

@ -1,5 +1,11 @@
# db
# db/Capacitor # db/Capacitor
La source peut être un iterable
---
charge() permet de spécifier la clé associée avec la valeur chargée, et charge() permet de spécifier la clé associée avec la valeur chargée, et
discharge() retourne les valeurs avec la clé primaire discharge() retourne les valeurs avec la clé primaire

View File

@ -25,14 +25,11 @@ class conds {
/** /**
* retourner une condition "like" si la valeur s'y prête * retourner une condition "like" si la valeur s'y prête
*
* - si la valeur fait moins de $likeThreshold caractères, faire une recherche * - si la valeur fait moins de $likeThreshold caractères, faire une recherche
* exacte en retournant ["=", $value] * exacte en retournant ["=", $value]
*
*
* - les espaces sont remplacés par % * - les espaces sont remplacés par %
* * - si $partial et que $value ne contient pas d'espaces, rajouter un % à la
* si $partial * fin
*/ */
static function like($value, bool $partial=false, ?int $likeThreshold=null) { static function like($value, bool $partial=false, ?int $likeThreshold=null) {
if ($value === false || $value === null) return $value; if ($value === false || $value === null) return $value;

View File

@ -3,12 +3,12 @@ namespace nulib\db\mysql;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
/** /**
* Class MysqlStorage * Class MysqlStorage
*/ */
class MysqlStorage extends CapacitorStorage { class MysqlCapacitor extends Capacitor {
function __construct($mysql) { function __construct($mysql) {
$this->db = Mysql::with($mysql); $this->db = Mysql::with($mysql);
} }
@ -36,21 +36,21 @@ class MysqlStorage extends CapacitorStorage {
"value" => "varchar(255)", "value" => "varchar(255)",
]; ];
function _getMigration(CapacitorChannel $channel): _mysqlMigration { function getMigration(CapacitorChannel $channel): _mysqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _mysqlMigration($migrations, $channel->getName()); return new _mysqlMigration($migrations, $channel->getName());
} }
const CHANNELS_COLS = [ const CATALOG_COLS = [
"name" => "varchar(255) not null primary key", "name" => "varchar(255) not null primary key",
"table_name" => "varchar(64)", "table_name" => "varchar(64)",
"class_name" => "varchar(255)", "class_name" => "varchar(255)",
]; ];
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [ return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on duplicate key update name = name", "suffix" => "on duplicate key update name = name",
]); ]);
} }

View File

@ -3,16 +3,16 @@ namespace nulib\db\pgsql;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
class PgsqlStorage extends CapacitorStorage { class PgsqlCapacitor extends Capacitor {
const SERDATA_DEFINITION = "text"; const CDATA_DEFINITION = "text";
const SERSUM_DEFINITION = "varchar(40)"; const CSUM_DEFINITION = "varchar(40)";
const SERTS_DEFINITION = "timestamp"; const CTIMESTAMP_DEFINITION = "timestamp";
const GENSERIAL_DEFINITION = "serial primary key"; const GSERIAL_DEFINITION = "serial primary key";
const GENTEXT_DEFINITION = "text"; const GTEXT_DEFINITION = "text";
const GENBOOL_DEFINITION = "boolean default false"; const GBOOL_DEFINITION = "boolean default false";
const GENUUID_DEFINITION = "uuid"; const GUUID_DEFINITION = "uuid";
function __construct($pgsql) { function __construct($pgsql) {
$this->db = Pgsql::with($pgsql); $this->db = Pgsql::with($pgsql);
@ -41,15 +41,15 @@ class PgsqlStorage extends CapacitorStorage {
return $found !== null; return $found !== null;
} }
function _getMigration(CapacitorChannel $channel): _pgsqlMigration { function getMigration(CapacitorChannel $channel): _pgsqlMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _pgsqlMigration($migrations, $channel->getName()); return new _pgsqlMigration($migrations, $channel->getName());
} }
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
return cl::merge(parent::_addToChannelsSql($channel), [ return cl::merge(parent::addToCatalogSql($channel), [
"suffix" => "on conflict (name) do nothing", "suffix" => "on conflict (name) do nothing",
]); ]);
} }

View File

@ -3,13 +3,13 @@ namespace nulib\db\sqlite;
use nulib\cl; use nulib\cl;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\CapacitorStorage; use nulib\db\Capacitor;
/** /**
* Class SqliteStorage * Class SqliteStorage
*/ */
class SqliteStorage extends CapacitorStorage { class SqliteCapacitor extends Capacitor {
const GENSERIAL_DEFINITION = "integer primary key autoincrement"; const GSERIAL_DEFINITION = "integer primary key autoincrement";
function __construct($sqlite) { function __construct($sqlite) {
$this->db = Sqlite::with($sqlite); $this->db = Sqlite::with($sqlite);
@ -31,30 +31,30 @@ class SqliteStorage extends CapacitorStorage {
return $found !== null; return $found !== null;
} }
function _getMigration(CapacitorChannel $channel): _sqliteMigration { function getMigration(CapacitorChannel $channel): _sqliteMigration {
$migrations = cl::merge([ $migrations = cl::merge([
"0init" => [$this->_createSql($channel)], "0init" => [$this->getCreateChannelSql($channel)],
], $channel->getMigration($this->db->getPrefix())); ], $channel->getMigration($this->db->getPrefix()));
return new _sqliteMigration($migrations, $channel->getName()); return new _sqliteMigration($migrations, $channel->getName());
} }
protected function _addToChannelsSql(CapacitorChannel $channel): array { protected function addToCatalogSql(CapacitorChannel $channel): array {
$sql = parent::_addToChannelsSql($channel); $sql = parent::addToCatalogSql($channel);
$sql[0] = "insert or ignore"; $sql[0] = "insert or ignore";
return $sql; return $sql;
} }
protected function _afterCreate(CapacitorChannel $channel): void { protected function afterCreate(CapacitorChannel $channel): void {
$db = $this->db; $db = $this->db;
if (!$this->tableExists(static::CHANNELS_TABLE)) { if (!$this->tableExists(static::CATALOG_TABLE)) {
# ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture # verrou en écriture
$db->exec($this->_createChannelsSql()); $db->exec($this->getCreateCatalogSql());
} }
if (!$this->channelExists($channel->getName())) { if (!$this->isInCatalog(["name" => $channel->getName()])) {
# ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un
# verrou en écriture # verrou en écriture
$db->exec($this->_addToChannelsSql($channel)); $db->exec($this->addToCatalogSql($channel));
} }
} }

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor; use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\mysql\Mysql; use nulib\db\mysql\Mysql;
use nulib\db\mysql\MysqlStorage; use nulib\db\mysql\MysqlCapacitor;
use nulib\output\msg; use nulib\output\msg;
use nulib\output\std\StdMessenger; use nulib\output\std\StdMessenger;
@ -35,7 +35,7 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new MysqlStorage($db), $channel = new MyChannel()); new Capacitor(new MysqlCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor; use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\pgsql\Pgsql; use nulib\db\pgsql\Pgsql;
use nulib\db\pgsql\PgsqlStorage; use nulib\db\pgsql\PgsqlCapacitor;
$db = new Pgsql([ $db = new Pgsql([
"host" => "pegase-dre.self", "host" => "pegase-dre.self",
@ -34,7 +34,7 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new PgsqlStorage($db), $channel = new MyChannel()); new Capacitor(new PgsqlCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -5,7 +5,7 @@ use nulib\cl;
use nulib\db\Capacitor; use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
use nulib\db\sqlite\Sqlite; use nulib\db\sqlite\Sqlite;
use nulib\db\sqlite\SqliteStorage; use nulib\db\sqlite\SqliteCapacitor;
$db = new Sqlite(__DIR__.'/test_sqlite.db'); $db = new Sqlite(__DIR__.'/test_sqlite.db');
@ -27,7 +27,7 @@ class MyChannel extends CapacitorChannel {
} }
} }
new Capacitor(new SqliteStorage($db), $channel = new MyChannel()); new Capacitor(new SqliteCapacitor($db), $channel = new MyChannel());
$channel->charge("hello world"); $channel->charge("hello world");
$channel->charge(["bonjour monde"]); $channel->charge(["bonjour monde"]);

View File

@ -30,23 +30,23 @@ class ChannelMigrationTest extends TestCase {
} }
function testMigration() { function testMigration() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$data = [ $data = [
["first", "premier", new DateTime(), new DateTime(), 15], ["first", "premier", new DateTime(), new DateTime(), 15],
["second", "deuxieme", new DateTime(), new DateTime(), 15], ["second", "deuxieme", new DateTime(), new DateTime(), 15],
]; ];
new Capacitor($storage, $channel = new MyChannel()); $capacitor->newChannel($channel = new MyChannel());
$channel->reset(true); $channel->reset(true);
$this->addData($channel, $data); $this->addData($channel, $data);
new Capacitor($storage, $channel = new MyChannelV2()); $capacitor->newChannel($channel = new MyChannelV2());
$this->addData($channel, $data); $this->addData($channel, $data);
new Capacitor($storage, $channel = new MyChannelV3()); $capacitor->newChannel($channel = new MyChannelV3());
$this->addData($channel, $data); $this->addData($channel, $data);
$sql = $channel->getCapacitor()->getCreateSql(); $sql = $channel->getCreateSql();
$class = MyChannelV3::class; $class = MyChannelV3::class;
$expected = <<<EOT $expected = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
@ -75,17 +75,17 @@ EOT;
} }
function testMigrationIndex() { function testMigrationIndex() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$data = [ $data = [
["un", "premier", "first"], ["un", "premier", "first"],
["deux", "deuxieme", "second"], ["deux", "deuxieme", "second"],
]; ];
new Capacitor($storage, $channel = new MyIndexChannel()); $capacitor->newChannel($channel = new MyIndexChannel());
$channel->reset(true); $channel->reset(true);
$channel->chargeAll($data); $channel->chargeAll($data);
$sql = $channel->getCapacitor()->getCreateSql(); $sql = $channel->getCreateSql();
$class = MyIndexChannel::class; $class = MyIndexChannel::class;
$expected = <<<EOT $expected = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

View File

@ -6,38 +6,48 @@ use nulib\cl;
use nulib\db\Capacitor; use nulib\db\Capacitor;
use nulib\db\CapacitorChannel; use nulib\db\CapacitorChannel;
class SqliteStorageTest extends TestCase { class SqliteCapacitorTest extends TestCase {
static function Txx(...$values): void { static function Txx(...$values): void {
foreach ($values as $value) { foreach ($values as $value) {
var_export($value); var_export($value);
} }
} }
function _testChargeStrings(SqliteStorage $storage, ?string $channel) { function _testChargeStrings(SqliteCapacitor $capacitor, CapacitorChannel $channel) {
$storage->reset($channel); $capacitor->reset($channel);
$storage->charge($channel, "first"); $capacitor->charge($channel, "first");
$storage->charge($channel, "second"); $capacitor->charge($channel, "second");
$storage->charge($channel, "third"); $capacitor->charge($channel, "third");
$items = cl::all($storage->discharge($channel, false)); $items = cl::all($capacitor->discharge($channel, false));
self::assertSame(["first", "second", "third"], $items); self::assertSame(["first", "second", "third"], $items);
} }
function _testChargeArrays(SqliteStorage $storage, ?string $channel) { function _testChargeArrays(SqliteCapacitor $capacitor, CapacitorChannel $channel) {
$storage->reset($channel); $capacitor->reset($channel);
$storage->charge($channel, ["id" => 10, "name" => "first"]); $capacitor->charge($channel, ["id" => 10, "name" => "first"]);
$storage->charge($channel, ["name" => "second", "id" => 20]); $capacitor->charge($channel, ["name" => "second", "id" => 20]);
$storage->charge($channel, ["name" => "third", "id" => "30"]); $capacitor->charge($channel, ["name" => "third", "id" => "30"]);
} }
function testChargeStrings() { function testChargeStrings() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$this->_testChargeStrings($storage, null);
$storage->close(); $channel = $capacitor->newChannel(null);
$this->_testChargeStrings($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$channel = $capacitor->newChannel("strings");
$this->_testChargeStrings($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$capacitor->close();
self::assertTrue(true);
} }
function testChargeArrays() { function testChargeArrays() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$storage->addChannel(new class extends CapacitorChannel {
$channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "arrays"; const NAME = "arrays";
const COLUMN_DEFINITIONS = ["id" => "integer"]; const COLUMN_DEFINITIONS = ["id" => "integer"];
@ -45,15 +55,16 @@ class SqliteStorageTest extends TestCase {
return ["id" => $item["id"] ?? null]; return ["id" => $item["id"] ?? null];
} }
}); });
$this->_testChargeArrays($capacitor, $channel);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$this->_testChargeStrings($storage, "strings"); $capacitor->close();
$this->_testChargeArrays($storage, "arrays"); self::assertTrue(true);
$storage->close();
} }
function testEach() { function testEach() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $each = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "each"; const NAME = "each";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"age" => "integer", "age" => "integer",
@ -67,11 +78,11 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($each);
$capacitor->charge(["name" => "first", "age" => 5]); $capacitor->charge($each, ["name" => "first", "age" => 5]);
$capacitor->charge(["name" => "second", "age" => 10]); $capacitor->charge($each, ["name" => "second", "age" => 10]);
$capacitor->charge(["name" => "third", "age" => 15]); $capacitor->charge($each, ["name" => "third", "age" => 15]);
$capacitor->charge(["name" => "fourth", "age" => 20]); $capacitor->charge($each, ["name" => "fourth", "age" => 20]);
$setDone = function ($row, $suffix=null) { $setDone = function ($row, $suffix=null) {
$item = $row["item"]; $item = $row["item"];
@ -82,17 +93,18 @@ class SqliteStorageTest extends TestCase {
} }
return $updates; return $updates;
}; };
$capacitor->each(["age" => [">", 10]], $setDone, ["++"]); $capacitor->each($each, ["age" => [">", 10]], $setDone, ["++"]);
$capacitor->each(["done" => 0], $setDone); $capacitor->each($each, ["done" => 0], $setDone);
self::Txx(cl::all($capacitor->discharge($each, false)));
self::Txx(cl::all($capacitor->discharge(false)));
$capacitor->close(); $capacitor->close();
self::assertTrue(true); self::assertTrue(true);
} }
function testPrimayKey() { function testPrimayKey() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "pk"; const NAME = "pk";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"id_" => "varchar primary key", "id_" => "varchar primary key",
@ -106,21 +118,23 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]); $capacitor->charge($channel, ["numero" => "a", "name" => "first", "age" => 5]);
$capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]); $capacitor->charge($channel, ["numero" => "b", "name" => "second", "age" => 10]);
$capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]); $capacitor->charge($channel, ["numero" => "c", "name" => "third", "age" => 15]);
$capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]); $capacitor->charge($channel, ["numero" => "d", "name" => "fourth", "age" => 20]);
sleep(2); sleep(2);
$capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]); $capacitor->charge($channel, ["numero" => "b", "name" => "second", "age" => 100]);
$capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]); $capacitor->charge($channel, ["numero" => "d", "name" => "fourth", "age" => 200]);
self::Txx(cl::all($capacitor->discharge($channel, false)));
$capacitor->close(); $capacitor->close();
self::assertTrue(true); self::assertTrue(true);
} }
function testSum() { function testSum() {
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "sum"; const NAME = "sum";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"a__" => "varchar", "a__" => "varchar",
@ -136,19 +150,17 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$capacitor->charge(["a" => null, "b" => null]); $capacitor->charge($channel, ["a" => null, "b" => null]);
$capacitor->charge(["a" => "first", "b" => "second"]); $capacitor->charge($channel, ["a" => "first", "b" => "second"]);
self::Txx("=== all"); self::Txx("=== all");
/** @var Sqlite $sqlite */ self::Txx(cl::all($capacitor->db()->all([
$sqlite = $capacitor->getStorage()->db();
self::Txx(cl::all($sqlite->all([
"select", "select",
"from" => $capacitor->getChannel()->getTableName(), "from" => $channel->getTableName(),
]))); ])));
self::Txx("=== each"); self::Txx("=== each");
$capacitor->each(null, function ($row) { $capacitor->each($channel, null, function ($row) {
self::Txx($row); self::Txx($row);
}); });
@ -158,8 +170,8 @@ class SqliteStorageTest extends TestCase {
function testEachValues() { function testEachValues() {
# tester que values contient bien toutes les valeurs de la ligne # tester que values contient bien toutes les valeurs de la ligne
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "each_values"; const NAME = "each_values";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar primary key", "name" => "varchar primary key",
@ -176,8 +188,8 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$capacitor->charge(["name" => "first", "age" => 5], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 5], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(5, $item["age"]); self::assertSame(5, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -189,7 +201,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($row, ["name", "age", "item"])); ], cl::select($row, ["name", "age", "item"]));
self::assertNull($prow); self::assertNull($prow);
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -211,7 +223,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($prow, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->each(null, function(array $row) { $capacitor->each($channel, null, function(array $row) {
$item = $row["item"]; $item = $row["item"];
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
@ -229,7 +241,7 @@ class SqliteStorageTest extends TestCase {
"notes" => "modified", "notes" => "modified",
]; ];
}); });
$capacitor->charge(["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 10], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(10, $item["age"]); self::assertSame(10, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -251,7 +263,7 @@ class SqliteStorageTest extends TestCase {
], cl::select($prow, ["name", "age", "done", "notes", "item"])); ], cl::select($prow, ["name", "age", "done", "notes", "item"]));
}); });
$capacitor->charge(["name" => "first", "age" => 20], function($item, ?array $row, ?array $prow) { $capacitor->charge($channel, ["name" => "first", "age" => 20], function($item, ?array $row, ?array $prow) {
self::assertSame("first", $item["name"]); self::assertSame("first", $item["name"]);
self::assertSame(20, $item["age"]); self::assertSame(20, $item["age"]);
self::assertnotnull($row); self::assertnotnull($row);
@ -276,8 +288,8 @@ class SqliteStorageTest extends TestCase {
function testSetItemNull() { function testSetItemNull() {
# tester le forçage de $îtem à null pour économiser la place # tester le forçage de $îtem à null pour économiser la place
$storage = new SqliteStorage(__DIR__.'/capacitor.db'); $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db');
$capacitor = new Capacitor($storage, new class extends CapacitorChannel { $channel = $capacitor->newChannel(new class extends CapacitorChannel {
const NAME = "set_item_null"; const NAME = "set_item_null";
const COLUMN_DEFINITIONS = [ const COLUMN_DEFINITIONS = [
"name" => "varchar primary key", "name" => "varchar primary key",
@ -294,8 +306,8 @@ class SqliteStorageTest extends TestCase {
} }
}); });
$capacitor->reset(); $capacitor->reset($channel);
$nbModified = $capacitor->charge(["name" => "first", "age" => 5], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 5], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 5, "name" => "first", "age" => 5,
"item" => $item, "item" => $item,
@ -306,7 +318,7 @@ class SqliteStorageTest extends TestCase {
sleep(1); sleep(1);
# nb: on met des sleep() pour que la date de modification soit systématiquement différente # nb: on met des sleep() pour que la date de modification soit systématiquement différente
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
@ -321,7 +333,7 @@ class SqliteStorageTest extends TestCase {
sleep(1); sleep(1);
# pas de modification ici # pas de modification ici
$nbModified = $capacitor->charge(["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 10], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 10, "name" => "first", "age" => 10,
"item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc", "item" => $item, "item__sum_" => "9181336dfca20c86313d6065d89aa2ad5070b0fc",
@ -335,7 +347,7 @@ class SqliteStorageTest extends TestCase {
self::assertSame(0, $nbModified); self::assertSame(0, $nbModified);
sleep(1); sleep(1);
$nbModified = $capacitor->charge(["name" => "first", "age" => 20], function ($item, ?array $row, ?array $prow) { $nbModified = $capacitor->charge($channel, ["name" => "first", "age" => 20], function ($item, ?array $row, ?array $prow) {
self::assertSame([ self::assertSame([
"name" => "first", "age" => 20, "name" => "first", "age" => 20,
"item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5", "item" => $item, "item__sum_" => "001b91982b4e0883b75428c0eb28573a5dc5f7a5",