diff --git a/.gitignore b/.gitignore index 62033b5..df06735 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/test_* + /.composer.lock.runphp .~lock*# diff --git a/pgsql.php b/pgsql.php deleted file mode 100644 index 918568f..0000000 --- a/pgsql.php +++ /dev/null @@ -1,81 +0,0 @@ - "pegase-dre.self", - "dbname" => "dre", - "user" => "root", - "password" => "admin", - #"user" => "reader", - #"password" => "reader", -]); - -function e($sql) { - global $pgsql; - $v = $pgsql->exec($sql); - $v = var_export($v, true); - echo "'$sql' --> $v\n"; -} -function g($sql) { - global $pgsql; - $v = $pgsql->get($sql); - $v = var_export($v, true); - echo "'$sql' --> $v\n"; -} -function o($sql) { - global $pgsql; - $r = $pgsql->one($sql); - $r = var_export($r, true); - echo "'$sql' --> $r\n"; -} -function a($sql) { - global $pgsql; - $rs = $pgsql->all($sql); - echo "'$sql'\n"; - foreach ($rs as $r) { - $r = var_export($r, true); - echo " --> $r\n"; - } -} - -g("select age from personnes where id=1"); -o("select name, age from personnes where id=2"); -a("select id, name, age from personnes"); - -$n = rand(); -$pgsql->exec([ - "insert", - "into" => "personnes", - "values" => [ - "name" => "prout$n", - "age" => $n, - ], -]); - -class MyChannel extends CapacitorChannel { - const COLUMN_DEFINITIONS = [ - "name" => "varchar", - "value" => "int", - ]; - - function getItemValues($item): ?array { - return $item; - } -} -$storage = new PgsqlStorage($pgsql); -$channel = new MyChannel(); -new Capacitor($storage, $channel); - -$channel->charge([ - "name" => "one", - "value" => rand(), -]); -foreach ($channel->all(null) as $row) { - var_export($row); -} \ No newline at end of file diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 70c6f46..aa5da52 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -120,10 +120,6 @@ class Capacitor implements ITransactor { if ($db->inTransaction()) $db->rollback(); } - function getCreateSql(): string { - return $this->storage->_getCreateSql($this->channel); - } - function exists(): bool { return $this->storage->_exists($this->channel); } diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index 97819d4..a17e679 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -17,6 +17,8 @@ class CapacitorChannel { const PRIMARY_KEYS = null; + const MIGRATION = null; + const MANAGE_TRANSACTIONS = true; const EACH_COMMIT_THRESHOLD = 100; @@ -80,6 +82,7 @@ class CapacitorChannel { } $this->columnDefinitions = $columnDefinitions; $this->primaryKeys = $primaryKeys; + $this->migration = cl::withn(static::MIGRATION); } protected string $name; @@ -192,6 +195,12 @@ class CapacitorChannel { return $this->columnDefinitions; } + protected ?array $migration; + + function getMigration(): ?array { + return $this->migration; + } + protected ?array $primaryKeys; function getPrimaryKeys(): ?array { diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 830e032..e2bc4ac 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -2,6 +2,7 @@ namespace nulib\db; use nulib\cl; +use nulib\db\_private\_migration; use nulib\db\cache\cache; use nulib\php\func; use nulib\ValueException; @@ -75,6 +76,10 @@ abstract class CapacitorStorage { return cl::merge($definitions, $constraints); } + protected function getMigration(CapacitorChannel $channel): ?array { + return $channel->getMigration(); + } + /** sérialiser les valeurs qui doivent l'être dans $values */ protected function serialize(CapacitorChannel $channel, ?array $values): ?array { if ($values === null) return null; @@ -137,11 +142,10 @@ abstract class CapacitorStorage { } protected function _createSql(CapacitorChannel $channel): array { - $cols = $this->ColumnDefinitions($channel); return [ "create table if not exists", "table" => $channel->getTableName(), - "cols" => $cols, + "cols" => $this->ColumnDefinitions($channel), ]; } @@ -156,12 +160,7 @@ $sql; EOT; } - abstract function _getCreateSql(CapacitorChannel $channel): string; - - /** obtenir la requête SQL utilisée pour créer la table */ - function getCreateSql(?string $channel): string { - return $this->_getCreateSql($this->getChannel($channel)); - } + abstract function _getMigration(CapacitorChannel $channel): _migration; protected function _afterCreate(CapacitorChannel $channel): void { } @@ -169,7 +168,7 @@ EOT; protected function _create(CapacitorChannel $channel): void { $channel->ensureSetup(); if (!$channel->isCreated()) { - $this->db()->exec($this->_createSql($channel)); + $this->_getMigration($channel)->migrate($this->db()); $this->_afterCreate($channel); $channel->setCreated(); } diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 030c594..29e83eb 100644 --- a/php/src/db/_private/_create.php +++ b/php/src/db/_private/_create.php @@ -18,28 +18,36 @@ class _create extends _common { static function parse(array $query, ?array &$bindings=null): string { #XXX implémentation minimale - $sql = [self::merge_seq($query)]; + $tmpsql = self::merge_seq($query); + self::consume('create(?:\s+table)?\b', $tmpsql); + $sql = ["create table"]; + if ($tmpsql) $sql[] = $tmpsql; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["table"]; + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; ## columns - $cols = $query["cols"]; - $index = 0; - foreach ($cols as $col => &$definition) { - if ($col === $index) { - $index++; - } else { - $definition = "$col $definition"; - } - }; unset($definition); - $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + $cols = $query["cols"] ?? null; + if ($cols !== null) { + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php index fd78dbb..55409d9 100644 --- a/php/src/db/_private/_delete.php +++ b/php/src/db/_private/_delete.php @@ -17,13 +17,16 @@ class _delete extends _common { #XXX implémentation minimale $tmpsql = self::merge_seq($query); self::consume('delete(?:\s+from)?\b', $tmpsql); - $sql = ["delete from", $tmpsql]; + $sql = ["delete from"]; + if ($tmpsql) $sql[] = $tmpsql; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["from"]; + $from = $query["from"] ?? null; + if ($from !== null) $sql[] = $from; ## where $where = $query["where"] ?? null; @@ -36,7 +39,8 @@ class _delete extends _common { } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php index 1fef213..a2ec549 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -9,7 +9,7 @@ class _generic extends _common { ]; static function isa(string $sql): bool { - return preg_match('/^(?:drop\s+table)\b/i', $sql); + return preg_match('/^drop\s+table\b/i', $sql); } static function parse(array $query, ?array &$bindings=null): string { diff --git a/php/src/db/_private/_migration.php b/php/src/db/_private/_migration.php new file mode 100644 index 0000000..8ad75a6 --- /dev/null +++ b/php/src/db/_private/_migration.php @@ -0,0 +1,45 @@ +db = $db; + $this->channel = $channel; + if ($migrations === null) $migrations = static::MIGRATION; + if ($migrations === null) $migrations = []; + elseif (is_string($migrations)) $migrations = [$migrations]; + elseif (is_callable($migrations)) $migrations = [$migrations]; + elseif (!is_array($migrations)) $migrations = [strval($migrations)]; + $this->migrations = $migrations; + } + + protected ?IDatabase $db; + + protected string $channel; + + /** @var callable[]|string[] */ + protected $migrations; + + abstract function setup(): void; + abstract function beforeMigrate(string $key): bool; + abstract function afterMigrate(string $key): void; + + function migrate(?IDatabase $db=null): void { + $db = ($this->db ??= $db); + $this->setup(); + foreach ($this->migrations as $key => $migration) { + if (!$this->beforeMigrate($key)) continue; + if (is_string($migration) || !func::is_callable($migration)) { + $db->exec($migration); + } else { + func::with($migration)->bind($this, true)->invoke([$db, $key]); + } + $this->afterMigrate($key); + } + } +} diff --git a/php/src/db/_private/_update.php b/php/src/db/_private/_update.php index 97cf75d..7b297c4 100644 --- a/php/src/db/_private/_update.php +++ b/php/src/db/_private/_update.php @@ -21,10 +21,12 @@ class _update extends _common { $sql = [self::merge_seq($query)]; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["table"]; + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; ## set self::parse_set_values($query["values"], $setsql, $bindings); @@ -42,7 +44,8 @@ class _update extends _common { } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 0c5e0f6..ced2c6f 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -23,7 +23,7 @@ class MysqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _mysqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/mysql/_mysqlQuery.php b/php/src/db/mysql/_mysqlQuery.php new file mode 100644 index 0000000..83cfa1b --- /dev/null +++ b/php/src/db/mysql/_mysqlQuery.php @@ -0,0 +1,7 @@ + $pdo->dbconn, "options" => $pdo->options, "config" => $pdo->config, - "migrate" => $pdo->migration, + "migration" => $pdo->migration, ], $params)); } else { return new static($pdo, $params); @@ -50,7 +50,7 @@ class Pdo implements IDatabase { protected const CONFIG = null; - protected const MIGRATE = null; + protected const MIGRATION = null; const dbconn_SCHEMA = [ "name" => "string", @@ -63,7 +63,7 @@ class Pdo implements IDatabase { "options" => ["?array|callable"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -94,7 +94,7 @@ class Pdo implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; if ($params["auto_open"] ?? $defaultAutoOpen) { @@ -145,7 +145,7 @@ class Pdo implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); @@ -217,7 +217,7 @@ class Pdo implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ @@ -242,7 +242,7 @@ class Pdo implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ diff --git a/php/src/db/pdo/query.php b/php/src/db/pdo/_pdoQuery.php similarity index 95% rename from php/src/db/pdo/query.php rename to php/src/db/pdo/_pdoQuery.php index 6fb05c9..c1161fa 100644 --- a/php/src/db/pdo/query.php +++ b/php/src/db/pdo/_pdoQuery.php @@ -5,7 +5,7 @@ use nulib\db\_private\_base; use nulib\db\_private\Tbindings; use nulib\output\msg; -class query extends _base { +class _pdoQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index 2fef081..b5bf9d8 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -21,7 +21,7 @@ class Pgsql implements IDatabase { "dbconn" => $pgsql->dbconn, "options" => $pgsql->options, "config" => $pgsql->config, - "migrate" => $pgsql->migration, + "migration" => $pgsql->migration, ], $params)); } else { return new static($pgsql, $params); @@ -37,14 +37,14 @@ class Pgsql implements IDatabase { const CONFIG = null; - const MIGRATE = null; + const MIGRATION = null; const params_SCHEMA = [ "dbconn" => ["array"], "options" => ["?array|callable"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -95,7 +95,7 @@ class Pgsql implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; if ($params["auto_open"] ?? $defaultAutoOpen) { @@ -183,7 +183,7 @@ class Pgsql implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $serialSupport = $this->options["serial_support"] ?? true; if ($serialSupport && $query->isInsert()) return $this->getLastSerial(); @@ -261,7 +261,7 @@ class Pgsql implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $row = pg_fetch_assoc($result); pg_free_result($result); @@ -277,7 +277,7 @@ class Pgsql implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $primaryKeys = cl::withn($primaryKeys); while (($row = pg_fetch_assoc($result)) !== false) { diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index f47078a..419e043 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -24,7 +24,7 @@ class PgsqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _pgsqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/pgsql/query.php b/php/src/db/pgsql/_pgsqlQuery.php similarity index 96% rename from php/src/db/pgsql/query.php rename to php/src/db/pgsql/_pgsqlQuery.php index 3ab5b6d..34c7ed1 100644 --- a/php/src/db/pgsql/query.php +++ b/php/src/db/pgsql/_pgsqlQuery.php @@ -6,7 +6,7 @@ use nulib\db\_private\_base; use nulib\db\_private\Tbindings; use nulib\output\msg; -class query extends _base { +class _pgsqlQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index 4e22e11..fffb8f1 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -30,7 +30,7 @@ class Sqlite implements IDatabase { "encryption_key" => $sqlite->encryptionKey, "allow_wal" => $sqlite->allowWal, "config" => $sqlite->config, - "migrate" => $sqlite->migration, + "migration" => $sqlite->migration, ], $params)); } elseif (is_array($sqlite)) { return new static(null, cl::merge($sqlite, $params)); @@ -72,7 +72,7 @@ class Sqlite implements IDatabase { const CONFIG = null; - const MIGRATE = null; + const MIGRATION = null; const params_SCHEMA = [ "file" => ["string", ""], @@ -81,7 +81,7 @@ class Sqlite implements IDatabase { "allow_wal" => ["?bool"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -109,7 +109,7 @@ class Sqlite implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $this->inTransaction = false; @@ -150,7 +150,7 @@ class Sqlite implements IDatabase { if ($this->db === null) { $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); _config::with($this->config)->configure($this); - _migration::with($this->migration)->migrate($this); + _sqliteMigration::with($this->migration)->migrate($this); $this->inTransaction = false; } return $this; @@ -183,7 +183,7 @@ class Sqlite implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); @@ -270,7 +270,7 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); @@ -315,7 +315,7 @@ class Sqlite implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index d304d93..3ba97d6 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -1,6 +1,7 @@ "integer primary key autoincrement", ]; + function _getMigration(CapacitorChannel $channel): _sqliteMigration { + return new _sqliteMigration(cl::merge([ + $this->_createSql($channel), + ], $channel->getMigration()), $channel->getName()); + } + function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _sqliteQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } @@ -51,8 +58,7 @@ class SqliteStorage extends CapacitorStorage { # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture $sqlite->exec([ - "create table if not exists", - "table" => "_channels", + "create table if not exists _channels", "cols" => [ "name" => "varchar primary key", "table_name" => "varchar", @@ -64,8 +70,7 @@ class SqliteStorage extends CapacitorStorage { # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture $sqlite->exec([ - "insert", - "into" => "_channels", + "insert into _channels", "values" => [ "name" => $channel->getName(), "table_name" => $channel->getTableName(), @@ -77,9 +82,15 @@ class SqliteStorage extends CapacitorStorage { } protected function _beforeReset(CapacitorChannel $channel): void { - $this->sqlite->exec([ - "delete", - "from" => "_channels", + $sqlite = $this->sqlite; + $sqlite->exec([ + "delete from _migration", + "where" => [ + "channel" => $channel->getName(), + ], + ]); + $sqlite->exec([ + "delete from _channels", "where" => [ "name" => $channel->getName(), ], diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php deleted file mode 100644 index cafb991..0000000 --- a/php/src/db/sqlite/_migration.php +++ /dev/null @@ -1,54 +0,0 @@ -migrations); - } else { - return new static($migrations); - } - } - - const MIGRATE = null; - - function __construct($migrations) { - if ($migrations === null) $migrations = static::MIGRATE; - if ($migrations === null) $migrations = []; - elseif (is_string($migrations)) $migrations = [$migrations]; - elseif (is_callable($migrations)) $migrations = [$migrations]; - elseif (!is_array($migrations)) $migrations = [strval($migrations)]; - $this->migrations = $migrations; - } - - /** @var callable[]|string[] */ - protected $migrations; - - function migrate(Sqlite $sqlite): void { - $sqlite->exec("create table if not exists _migration(key varchar primary key, value varchar not null, done integer default 0)"); - foreach ($this->migrations as $key => $migration) { - $exists = $sqlite->get("select 1 from _migration where key = :key and done = 1", [ - "key" => $key, - ]); - if (!$exists) { - $sqlite->exec("insert or replace into _migration(key, value, done) values(:key, :value, :done)", [ - "key" => $key, - "value" => $migration, - "done" => 0, - ]); - if (is_string($migration) && !func::is_method($migration)) { - $sqlite->exec($migration); - } else { - func::with($migration)->bind($this, true)->invoke([$sqlite, $key]); - } - $sqlite->exec("update _migration set done = 1 where key = :key", [ - "key" => $key, - ]); - } - } - } -} diff --git a/php/src/db/sqlite/_sqliteMigration.php b/php/src/db/sqlite/_sqliteMigration.php new file mode 100644 index 0000000..1fdca37 --- /dev/null +++ b/php/src/db/sqlite/_sqliteMigration.php @@ -0,0 +1,57 @@ +db->exec([ + "create table if not exists _migration", + "cols" => [ + "channel" => "varchar not null", + "key" => "varchar not null", + "done" => "integer not null default 0", + ], + ]); + } + + function beforeMigrate(string $key): bool { + $db = $this->db; + $migrated = $db->get([ + "select 1 from _migration", + "where" => [ + "channel" => $this->channel, + "key" => $key, + "done" => 1, + ], + ]); + if ($migrated) return false; + $db->exec([ + "insert or replace into _migration", + "values" => [ + "channel" => $this->channel, + "key" => $key, + "done" => 0, + ], + ]); + return true; + } + + function afterMigrate(string $key): void { + $this->db->exec([ + "update _migration", + "values" => [ + "done" => 1, + ], + "where" => [ + "channel" => $this->channel, + "key" => $key, + ], + ]); + } +} diff --git a/php/src/db/sqlite/query.php b/php/src/db/sqlite/_sqliteQuery.php similarity index 97% rename from php/src/db/sqlite/query.php rename to php/src/db/sqlite/_sqliteQuery.php index e8df936..14fb1b0 100644 --- a/php/src/db/sqlite/query.php +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -14,7 +14,7 @@ use nulib\ValueException; use SQLite3; use SQLite3Stmt; -class query extends _base { +class _sqliteQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/php/func.php b/php/src/php/func.php index 1309ec8..26361f7 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -437,7 +437,7 @@ class func { return new ValueException($reason); } - static function with($func, ?array $args=null, bool $strict=true): self { + private static function _with($func, ?array $args=null, bool $strict=true, ?string &$reason=null): ?self { if (!is_array($func)) { if ($func instanceof Closure) { return new self(self::TYPE_CLOSURE, $func, $args); @@ -458,6 +458,12 @@ class func { } elseif (self::verifix_static($func, $strict, $bound, $reason)) { return new self(self::TYPE_STATIC, $func, $args, $bound, $reason); } + return null; + } + + static function with($func, ?array $args=null, bool $strict=true): self { + $func = self::_with($func, $args, $strict, $reason); + if ($func !== null) return $func; throw self::not_a_callable($func, $reason); } @@ -478,6 +484,13 @@ class func { } } + static function is_callable($func): bool { + $func = self::_with($func); + if ($func === null) return false; + if (!$func->isBound()) return false; + return $func->type !== self::TYPE_CLASS; + } + static function call($func, ...$args) { return self::with($func)->invoke($args); } @@ -638,7 +651,7 @@ class func { function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; - if ($this->object !== null && $unlessAlreadyBound) return $this; + if ($this->bound && $unlessAlreadyBound) return $this; [$c, $f] = $this->func; if ($replace) { diff --git a/php/tests/db/sqlite/SqliteTest.php b/php/tests/db/sqlite/SqliteTest.php index b56855c..a6e23a1 100644 --- a/php/tests/db/sqlite/SqliteTest.php +++ b/php/tests/db/sqlite/SqliteTest.php @@ -11,7 +11,7 @@ class SqliteTest extends TestCase { function testMigration() { $sqlite = new Sqlite(":memory:", [ - "migrate" => [ + "migration" => [ self::CREATE_PERSON, self::INSERT_JEPHTE, ], @@ -49,7 +49,7 @@ class SqliteTest extends TestCase { } function testInsert() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table mapping (i integer, s varchar)", + "migration" => "create table mapping (i integer, s varchar)", ]); $sqlite->exec(["insert into mapping", "values" => ["i" => 1, "s" => "un"]]); $sqlite->exec(["insert mapping", "values" => ["i" => 2, "s" => "deux"]]); @@ -78,7 +78,7 @@ class SqliteTest extends TestCase { function testSelect() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table user (name varchar, amount integer)", + "migration" => "create table user (name varchar, amount integer)", ]); $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 2]]); @@ -130,7 +130,7 @@ class SqliteTest extends TestCase { function testSelectGroupBy() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table user (name varchar, amount integer)", + "migration" => "create table user (name varchar, amount integer)", ]); $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 1]]); diff --git a/php/tests/db/sqlite/_queryTest.php b/php/tests/db/sqlite/_queryTest.php index 3337eb5..3d069dd 100644 --- a/php/tests/db/sqlite/_queryTest.php +++ b/php/tests/db/sqlite/_queryTest.php @@ -6,119 +6,119 @@ use PHPUnit\Framework\TestCase; class _queryTest extends TestCase { function testParseConds(): void { $sql = $params = null; - query::parse_conds(null, $sql, $params); + _sqliteQuery::parse_conds(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_conds([], $sql, $params); + _sqliteQuery::parse_conds([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["col" => null], $sql, $params); + _sqliteQuery::parse_conds(["col" => null], $sql, $params); self::assertSame(["col is null"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["col = 'value'"], $sql, $params); + _sqliteQuery::parse_conds(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds([["col = 'value'"]], $sql, $params); + _sqliteQuery::parse_conds([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["int" => 42, "string" => "value"], $sql, $params); + _sqliteQuery::parse_conds(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["(int = :int and string = :string)"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - query::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); + _sqliteQuery::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); self::assertSame(["(int = :int or string = :string)"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - query::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + _sqliteQuery::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); $sql = $params = null; - query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + _sqliteQuery::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); self::assertSame(["(int is null and string <> :string)"], $sql); self::assertSame(["string" => "value"], $params); $sql = $params = null; - query::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); self::assertSame(["col between :col and :col2"], $sql); self::assertSame(["col" => "lower", "col2" => "upper"], $params); $sql = $params = null; - query::parse_conds(["col" => ["in", "one"]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["in", "one"]], $sql, $params); self::assertSame(["col in (:col)"], $sql); self::assertSame(["col" => "one"], $params); $sql = $params = null; - query::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); self::assertSame(["col in (:col, :col2)"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col and col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col or col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col and col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col or col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); } function testParseValues(): void { $sql = $params = null; - query::parse_set_values(null, $sql, $params); + _sqliteQuery::parse_set_values(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values([], $sql, $params); + _sqliteQuery::parse_set_values([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values(["col = 'value'"], $sql, $params); + _sqliteQuery::parse_set_values(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values([["col = 'value'"]], $sql, $params); + _sqliteQuery::parse_set_values([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + _sqliteQuery::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["int = :int", "string = :string"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + _sqliteQuery::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["int = :int", "string = :string"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - query::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + _sqliteQuery::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); }