diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index aa5da52..0fee8b5 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -39,6 +39,11 @@ class Capacitor implements ITransactor { 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; diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index b0649ae..c7265bb 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -66,26 +66,47 @@ class CapacitorChannel implements ITransactor { $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); $primaryKeys = cl::withn(static::PRIMARY_KEYS); $migration = cl::withn(static::MIGRATION); + $lastMkey = 1; if ($columnDefinitions !== null) { # mettre à jour la liste des clés primaires et des migrations $index = 0; foreach ($columnDefinitions as $col => $def) { if ($col === $index) { - # si définition séquentielle, seules les définitions de clé - # primaires sont supportées $index++; - if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { - $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); - } - } elseif (is_array($def)) { - # tableau: c'est une migration - $def = implode(" ", $def); - if ($def) { - $migration["add_$col"] = "alter table $tableName add column $col $def"; + if (is_array($def)) { + # tableau: c'est une migration + $mkey = null; + $mvalues = null; + $mdefs = $def; + $mindex = 0; + foreach ($mdefs as $mcol => $mdef) { + if ($mindex === 0 && $mcol === 0) { + $mindex++; + $mkey = $mdef; + } elseif ($mcol === $mindex) { + # si définition séquentielle, prendre la migration telle quelle + $mindex++; + $mvalues[] = $mdef; + } elseif ($mdef) { + # mise à jour d'une colonne + $mvalues[] = "alter table $tableName add column $mcol $mdef"; + } else { + # suppression d'une colonne + $mvalues[] = "alter table $tableName drop column $mcol"; + } + } + if ($mvalues !== null) { + if ($mkey === null) $mkey = $lastMkey++; + $migration[$mkey] = $mvalues; + } } else { - $migration["drop_$col"] = "alter table $tableName drop column $col"; + # si définition séquentielle, seules les définitions de clé + # primaires sont supportées + if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { + $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); + } } - } elseif (is_scalar($def)) { + } else { # chaine: c'est une définition $def = strval($def); if (preg_match('/\bprimary\s+key\b/i', $def)) { diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 2801fca..e7ff6af 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -40,6 +40,16 @@ abstract class CapacitorStorage { const SERSUM_DEFINITION = "varchar(40)"; const SERTS_DEFINITION = "datetime"; + protected static function sercol($def): string { + if (!is_string($def)) $def = strval($def); + switch ($def) { + case "serdata": $def = static::SERDATA_DEFINITION; break; + case "sersum": $def = static::SERSUM_DEFINITION; break; + case "serts": $def = static::SERTS_DEFINITION; break; + } + return $def; + } + const COLUMN_DEFINITIONS = [ "item__" => "serdata", "item__sum_" => "sersum", @@ -61,22 +71,29 @@ abstract class CapacitorStorage { $constraints = []; $index = 0; foreach ($tmp as $col => $def) { - switch ($def) { - case "serdata": $def = static::SERDATA_DEFINITION; break; - case "sersum": $def = static::SERSUM_DEFINITION; break; - case "serts": $def = static::SERTS_DEFINITION; break; - } if ($col === $index) { $index++; - $constraints[] = $def; - } elseif (is_array($def)) { - # éventuellement, ignorer les migrations - $def = implode(" ", $def); - if ($def && !$ignoreMigrations) { - $definitions[$col] = $def; + 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::sercol($mdef); + } else { + unset($definitions[$mcol]); + } + } + } + } + } else { + $constraints[] = $def; } - } elseif (is_scalar($def)) { - $definitions[$col] = strval($def); + } else { + $definitions[$col] = self::sercol($def); } } return cl::merge($definitions, $constraints); @@ -155,17 +172,6 @@ abstract class CapacitorStorage { ]; } - protected static function format_sql(CapacitorChannel $channel, string $sql): string { - $class = get_class($channel); - return <<exec([ "drop table if exists", - "table" => _migration::MIGRATION_TABLE + "table" => _migration::MIGRATION_TABLE, ]); $db->exec([ "create table", @@ -388,7 +394,7 @@ EOT; if ($func !== null) { $updates = func::with($func) ->prependArgs([$item, $values, $pvalues]) - ->bind($channel, true) + ->bind($channel) ->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { @@ -603,7 +609,7 @@ EOT; 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, true); + $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(); @@ -671,7 +677,7 @@ EOT; function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int { $this->_create($channel); if ($func === null) $func = CapacitorChannel::onDelete; - $onEach = func::with($func)->bind($channel, true); + $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(); diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php index dc71720..1383c9a 100644 --- a/php/src/db/IDatabase.php +++ b/php/src/db/IDatabase.php @@ -2,6 +2,9 @@ namespace nulib\db; interface IDatabase extends ITransactor { + /** obtenir la requête SQL correspondant à $query */ + function getSql($query, ?array $params=null): string; + /** * - si c'est un insert, retourner l'identifiant autogénéré de la ligne * - sinon retourner le nombre de lignes modifiées en cas de succès, ou false diff --git a/php/src/db/_private/_base.php b/php/src/db/_private/_base.php index 5e366e6..2ca42f9 100644 --- a/php/src/db/_private/_base.php +++ b/php/src/db/_private/_base.php @@ -1,8 +1,6 @@ exec($config); } else { - func::with($config)->bind($this, true)->invoke([$db, $key]); + func::with($config)->bind($this)->invoke([$db, $key]); } } } diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php index dea829c..d71e157 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -1,9 +1,7 @@ migrations as $name => $migration) { if ($this->isMigrated($name)) continue; $this->setMigrated($name, false); - if (is_string($migration) || !func::is_callable($migration)) { - $db->exec($migration); + if (func::is_callable($migration)) { + func::with($migration)->bind($this)->invoke([$db, $name]); } else { - func::with($migration)->bind($this, true)->invoke([$db, $name]); + foreach (cl::with($migration) as $query) { + $db->exec($query); + } } $this->setMigrated($name, true); } } + + protected static function sql_prefix(?string $source=null): string { + $prefix = "-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8\n"; + if ($source !== null) $prefix .= "-- autogénéré à partir de $source\n"; + return $prefix; + } + + function getSql(?string $source=null, ?IDatabase $db=null): string { + $db = ($this->db ??= $db); + $lines = [self::sql_prefix($source)]; + foreach ($this->migrations as $name => $migration) { + $lines[] = "-- $name"; + if (func::is_callable($migration)) { + $lines[] = "-- "; + } else { + foreach (cl::with($migration) as $query) { + $sql = $db->getSql($query); + $lines[] = "$sql;"; + } + } + $lines[] = ""; + } + return implode("\n", $lines); + } } diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 4f27f1a..ace8beb 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -41,14 +41,10 @@ class MysqlStorage extends CapacitorStorage { ]; function _getMigration(CapacitorChannel $channel): _mysqlMigration { - return new _mysqlMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _mysqlQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _mysqlMigration($migrations, $channel->getName()); } const CHANNELS_COLS = [ diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 2a970fa..f34ff69 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -1,7 +1,6 @@ getSql(); + } + function open(): self { if ($this->db === null) { $dbconn = $this->dbconn; $options = $this->options; if (is_callable($options)) { - $options = func::with($options)->bind($this, true)->invoke(); + $options = func::with($options)->bind($this)->invoke(); } $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); _config::with($this->config)->configure($this); diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index b5bf9d8..dbcbedf 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -117,6 +117,11 @@ class Pgsql implements IDatabase { /** @var resource */ protected $db = null; + function getSql($query, ?array $params=null): string { + $query = new _pgsqlQuery($query, $params); + return $query->getSql(); + } + function open(): self { if ($this->db === null) { $dbconn = $this->dbconn; @@ -136,7 +141,7 @@ class Pgsql implements IDatabase { $connection_string = implode(" ", array_filter($connection_string)); $options = $this->options; if (is_callable($options)) { - $options = func::with($options)->bind($this, true)->invoke(); + $options = func::with($options)->bind($this)->invoke(); } $forceNew = $options["force_new"] ?? false; $flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0; diff --git a/php/src/db/pgsql/PgsqlException.php b/php/src/db/pgsql/PgsqlException.php index afd21c4..f9a500e 100644 --- a/php/src/db/pgsql/PgsqlException.php +++ b/php/src/db/pgsql/PgsqlException.php @@ -3,7 +3,6 @@ namespace nulib\db\pgsql; use Exception; use RuntimeException; -use SQLite3; class PgsqlException extends RuntimeException { static final function last_error($db): self { diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index 0dc93b7..dd89e2a 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -42,14 +42,10 @@ class PgsqlStorage extends CapacitorStorage { } function _getMigration(CapacitorChannel $channel): _pgsqlMigration { - return new _pgsqlMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _pgsqlQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _pgsqlMigration($migrations, $channel->getName()); } protected function _addToChannelsSql(CapacitorChannel $channel): array { diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index fffb8f1..ae4ea99 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -146,6 +146,11 @@ class Sqlite implements IDatabase { protected bool $inTransaction; + function getSql($query, ?array $params=null): string { + $query = new _sqliteQuery($query, $params); + return $query->getSql(); + } + function open(): self { if ($this->db === null) { $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index f031a56..4621cb2 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -34,14 +34,10 @@ class SqliteStorage extends CapacitorStorage { } function _getMigration(CapacitorChannel $channel): _sqliteMigration { - return new _sqliteMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _sqliteQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _sqliteMigration($migrations, $channel->getName()); } function channelExists(string $name): bool { diff --git a/php/src/db/sqlite/_sqliteQuery.php b/php/src/db/sqlite/_sqliteQuery.php index 14fb1b0..04e6f1c 100644 --- a/php/src/db/sqlite/_sqliteQuery.php +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -2,15 +2,8 @@ namespace nulib\db\sqlite; use nulib\db\_private\_base; -use nulib\db\_private\_create; -use nulib\db\_private\_delete; -use nulib\db\_private\_generic; -use nulib\db\_private\_insert; -use nulib\db\_private\_select; -use nulib\db\_private\_update; use nulib\db\_private\Tbindings; use nulib\output\msg; -use nulib\ValueException; use SQLite3; use SQLite3Stmt; diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index 2affcc8..3ec767d 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -35,7 +35,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { $this->rows = $rows; $this->index = 0; $cookFunc = $params["cook_func"] ?? null; - if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this, true); + if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this); $this->cookFunc = $cookFunc; $this->output = $params["output"] ?? static::OUTPUT; $maxMemory = $params["max_memory"] ?? null; diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php index 4105846..ebf4c52 100644 --- a/php/src/php/content/c.php +++ b/php/src/php/content/c.php @@ -82,7 +82,7 @@ class c { $arg = self::resolve($arg, $object_or_class, false); if (!$array) $arg = $arg[0]; }; unset($arg); - $value = func::with($func, $args)->bind($object_or_class, true)->invoke(); + $value = func::with($func, $args)->bind($object_or_class)->invoke(); } } if ($seq) $dest[] = $value; diff --git a/php/src/php/func.php b/php/src/php/func.php index 26361f7..675d359 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -3,7 +3,6 @@ namespace nulib\php; use Closure; use Exception; -use Generator; use nulib\A; use nulib\cl; use nulib\cv; @@ -268,6 +267,8 @@ class func { $reason = "$c::$f: method not found"; return false; } + $method = new ReflectionMethod($c, $f); + if (!$method->isStatic()) return false; } else { $reason = "$c::$f: not bound"; } @@ -402,6 +403,8 @@ class func { $reason = "$c::$f: method not found"; return false; } + $method = new ReflectionMethod($c, $f); + if ($method->isStatic()) return false; } else { $reason = "$c::$f: not bound"; } @@ -649,9 +652,9 @@ class func { else return $this->bound && $this->object !== null; } - function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self { + function bind($object, bool $rebind=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; - if ($this->bound && $unlessAlreadyBound) return $this; + if (!$rebind && $this->isBound()) return $this; [$c, $f] = $this->func; if ($replace) { diff --git a/php/src/str.php b/php/src/str.php index b0f3497..347726b 100644 --- a/php/src/str.php +++ b/php/src/str.php @@ -268,6 +268,21 @@ class str { $s .= $prefix.$text.$suffix; } + /** + * dans $s, faire les remplacements $key => $value du tableau $replaces + * + * si $verifix_order, le tableau est réordonné par taille de chaine source + */ + static final function replace(?string $s, ?array $replaces, bool $verifix_order=true): ?string { + if ($s === null || $replaces === null) return $s; + if ($verifix_order) { + uksort($replaces, function ($a, $b) { + return -cv::compare(strlen($a), strlen($b)); + }); + } + return str_replace(array_keys($replaces), array_values($replaces), $s); + } + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { self::addsep($s, " ", $text, $prefix, $suffix); diff --git a/php/tests/db/sqlite/ChannelMigrationTest.php b/php/tests/db/sqlite/ChannelMigrationTest.php new file mode 100644 index 0000000..1946179 --- /dev/null +++ b/php/tests/db/sqlite/ChannelMigrationTest.php @@ -0,0 +1,75 @@ +charge([ + "name" => $name, + "value" => $value, + "date_cre" => $dateCre, + "date_mod" => $dateMod, + "age" => $age, + ]); + } + } + + function testMigration() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $data = [ + ["first", "premier", new DateTime(), new DateTime(), 15], + ["second", "deuxieme", new DateTime(), new DateTime(), 15], + ]; + + new Capacitor($storage, $channel = new MyChannel()); + $channel->reset(true); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV2()); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV3()); + $this->addData($channel, $data); + + $sql = $channel->getCapacitor()->getCreateSql(); + $class = MyChannelV3::class; + $expected = << "varchar not null primary key", + "value" => "varchar", + ]; + + const VERSION = 1; + + function __construct() { + parent::__construct(); + $this->version = static::VERSION; + } + + protected int $version; + + function getItemValues($item): ?array { + + return [ + "name" => "{$item["name"]}$this->version", + "value" => "{$item["value"]} v$this->version", + "date_cre" => $item["date_cre"] ?? null, + "date_mod" => $item["date_mod"] ?? null, + "age" => $item["age"] ?? null, + ]; + } +} diff --git a/php/tests/db/sqlite/impl/MyChannelV2.php b/php/tests/db/sqlite/impl/MyChannelV2.php new file mode 100644 index 0000000..68f18bf --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV2.php @@ -0,0 +1,14 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ]; +} diff --git a/php/tests/db/sqlite/impl/MyChannelV3.php b/php/tests/db/sqlite/impl/MyChannelV3.php new file mode 100644 index 0000000..e6e2170 --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV3.php @@ -0,0 +1,17 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ["infos", + "age" => "integer", + ], + ]; +} diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index 45a0581..f15ce27 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -562,7 +562,7 @@ namespace nulib\php { true, true, [SC::class, "tstatic"], ], [[SC::class, "tmethod"], - true, true, [SC::class, "tmethod"], + false, true, [SC::class, "tmethod"], true, true, [SC::class, "tmethod"], ], [[SC::class, "->tmethod"], @@ -1102,15 +1102,25 @@ namespace nulib\php { } function testRebind() { + # bind if not already bound + $func = func::with([C1::class, "tmethod"]); + // bind + self::assertSame(11, $func->bind(new C1(0))->invoke()); + // pas de bind, puis que déjà bound + self::assertSame(11, $func->bind(new C1(1))->invoke()); + // même si l'objet est de type différent, pas de bind + self::assertSame(11, $func->bind(new C0())->invoke()); + + # force rebind $func = func::with([C1::class, "tmethod"]); // objets du même type - self::assertSame(11, $func->bind(new C1(0))->invoke()); - self::assertSame(12, $func->bind(new C1(1))->invoke()); + self::assertSame(11, $func->bind(new C1(0), true)->invoke()); + self::assertSame(12, $func->bind(new C1(1), true)->invoke()); // objets de type différent self::assertException(ValueException::class, function() use ($func) { - $func->bind(new C0())->invoke(); + $func->bind(new C0(), true)->invoke(); }); - self::assertSame(11, $func->bind(new C0(), false, true)->invoke()); + self::assertSame(11, $func->bind(new C0(), true, true)->invoke()); } function testModifyArgs() { diff --git a/php/tests/strTest.php b/php/tests/strTest.php index 2d69303..c03997e 100644 --- a/php/tests/strTest.php +++ b/php/tests/strTest.php @@ -5,7 +5,22 @@ namespace nulib; use nulib\tests\TestCase; class strTest extends TestCase { - function testSplit_tokens() { + function test_replace() { + self::assertSame("premier deuxieme", str::replace("first second", [ + "first" => "premier", + "second" => "deuxieme", + ])); + self::assertSame("avant OK", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ])); + self::assertSame("avant avantsuffix", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ], false)); + } + + function test_split_tokens() { self::assertNull(str::split_tokens(null)); self::assertSame([], str::split_tokens("")); self::assertSame(["token"], str::split_tokens("token")); @@ -13,7 +28,7 @@ class strTest extends TestCase { self::assertSame(["t", "u", "v", "w"], str::split_tokens("\nt\n\nu\r\nv\rw")); } - function testCamel2us() { + function test_camel2us() { self::assertSame("a", str::camel2us("a")); self::assertSame("aa", str::camel2us("aa")); self::assertSame("aaa", str::camel2us("aaa"));