améliorer le support des migration dans les canaux

This commit is contained in:
Jephté Clain 2025-04-28 09:21:47 +04:00
parent 9767028da6
commit 37354525ec
21 changed files with 274 additions and 79 deletions

View File

@ -39,6 +39,12 @@ class Capacitor implements ITransactor {
return $this->getChannel()->getTableName();
}
function getCreateSql(): string {
$storage = $this->storage;
$channel = $this->channel;
return $storage->_getMigration($channel)->getSql(get_class($channel), $this->db());
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;

View File

@ -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)) {

View File

@ -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 <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
-- autogénéré à partir de $class
$sql;
EOT;
}
abstract protected function tableExists(string $tableName): bool;
const METADATA_TABLE = "_metadata";
@ -183,7 +189,7 @@ EOT;
]);
$db->exec([
"drop table if exists",
"table" => _migration::MIGRATION_TABLE
"table" => _migration::MIGRATION_TABLE,
]);
$db->exec([
"create table",

View File

@ -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

View File

@ -1,8 +1,6 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
abstract class _base extends _common {

View File

@ -1,9 +1,7 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\str;
use nulib\ValueException;
class _generic extends _common {
const SCHEMA = [

View File

@ -1,6 +1,7 @@
<?php
namespace nulib\db\_private;
use nulib\cl;
use nulib\db\IDatabase;
use nulib\php\func;
@ -61,12 +62,38 @@ abstract class _migration {
foreach ($this->migrations as $name => $migration) {
if ($this->isMigrated($name)) continue;
$this->setMigrated($name, false);
if (is_string($migration) || !func::is_callable($migration)) {
$db->exec($migration);
} else {
if (func::is_callable($migration)) {
func::with($migration)->bind($this)->invoke([$db, $name]);
} else {
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[] = "-- <fonction PHP>";
} else {
foreach (cl::with($migration) as $query) {
$sql = $db->getSql($query);
$lines[] = "$sql;";
}
}
$lines[] = "";
}
return implode("\n", $lines);
}
}

View File

@ -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 = [

View File

@ -1,7 +1,6 @@
<?php
namespace nulib\db\pdo;
use Generator;
use nulib\cl;
use nulib\db\_private\_config;
use nulib\db\_private\Tvalues;
@ -115,6 +114,11 @@ class Pdo implements IDatabase {
protected ?\PDO $db = null;
function getSql($query, ?array $params=null): string {
$query = new _pdoQuery($query, $params);
return $query->getSql();
}
function open(): self {
if ($this->db === null) {
$dbconn = $this->dbconn;

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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 {

View File

@ -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;

View File

@ -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";
}

View File

@ -0,0 +1,75 @@
<?php
namespace nulib\db\sqlite;
use nulib\db\Capacitor;
use nulib\db\sqlite\impl\MyChannel;
use nulib\db\sqlite\impl\MyChannelV2;
use nulib\db\sqlite\impl\MyChannelV3;
use nulib\output\msg;
use nulib\output\std\StdMessenger;
use nulib\php\time\DateTime;
use nulib\tests\TestCase;
class ChannelMigrationTest extends TestCase {
static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
msg::set_messenger_class(StdMessenger::class);
}
protected function addData(MyChannel $channel, array $data): void {
foreach ($data as [$name, $value, $dateCre, $dateMod, $age]) {
$channel->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 = <<<EOT
-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
-- autogénéré à partir de $class
-- 0init
create table if not exists my (
id_ integer primary key autoincrement
, name varchar
, value varchar
, item__ mediumtext
, item__sum_ varchar(40)
, created_ datetime
, modified_ datetime
);
-- dates
alter table my add column date_cre datetime;
alter table my add column date_mod datetime;
-- infos
alter table my add column age integer;
EOT;
self::assertSame($expected, $sql);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace nulib\db\sqlite\impl;
use nulib\db\CapacitorChannel;
class MyChannel extends CapacitorChannel {
const NAME = "my";
const TABLE_NAME = "my";
const COLUMN_DEFINITIONS = [
"name" => "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,
];
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace nulib\db\sqlite\impl;
class MyChannelV2 extends MyChannel {
const VERSION = 2;
const COLUMN_DEFINITIONS = [
"name" => "varchar",
"value" => "varchar",
["dates",
"date_cre" => "datetime",
"date_mod" => "datetime",
],
];
}

View File

@ -0,0 +1,17 @@
<?php
namespace nulib\db\sqlite\impl;
class MyChannelV3 extends MyChannel {
const VERSION = 3;
const COLUMN_DEFINITIONS = [
"name" => "varchar",
"value" => "varchar",
["dates",
"date_cre" => "datetime",
"date_mod" => "datetime",
],
["infos",
"age" => "integer",
],
];
}

View File

@ -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"],