Added database migration utility.
This commit is contained in:
parent
f8c6602ab9
commit
fbe4fe18de
8 changed files with 277 additions and 6 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2301.62020
|
||||
0.2301.70411
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// MariaDBConnection.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\MariaDB;
|
||||
|
||||
|
@ -362,7 +362,11 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
* @return MariaDBStatement A database statement.
|
||||
*/
|
||||
public function prepare(string $query): MariaDBStatement {
|
||||
$statement = $this->connection->prepare($query);
|
||||
try {
|
||||
$statement = $this->connection->prepare($query);
|
||||
} catch(mysqli_sql_exception $ex) {
|
||||
throw new QueryExecuteException($ex->getMessage(), $ex->getCode(), $ex);
|
||||
}
|
||||
if($statement === false)
|
||||
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
|
||||
return new MariaDBStatement($statement);
|
||||
|
@ -372,7 +376,11 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
* @return MariaDBResult A database result.
|
||||
*/
|
||||
public function query(string $query): MariaDBResult {
|
||||
$result = $this->connection->query($query, MYSQLI_STORE_RESULT);
|
||||
try {
|
||||
$result = $this->connection->query($query, MYSQLI_STORE_RESULT);
|
||||
} catch(mysqli_sql_exception $ex) {
|
||||
throw new QueryExecuteException($ex->getMessage(), $ex->getCode(), $ex);
|
||||
}
|
||||
if($result === false)
|
||||
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
|
||||
// Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
|
||||
|
@ -380,8 +388,12 @@ class MariaDBConnection implements IDbConnection, IDbTransactions {
|
|||
}
|
||||
|
||||
public function execute(string $query): int|string {
|
||||
if(!$this->connection->real_query($query))
|
||||
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
|
||||
try {
|
||||
if(!$this->connection->real_query($query))
|
||||
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
|
||||
} catch(mysqli_sql_exception $ex) {
|
||||
throw new QueryExecuteException($ex->getMessage(), $ex->getCode(), $ex);
|
||||
}
|
||||
return $this->connection->affected_rows;
|
||||
}
|
||||
|
||||
|
|
148
src/Data/Migration/DbMigrationManager.php
Normal file
148
src/Data/Migration/DbMigrationManager.php
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
// DbMigrationManager.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use stdClass;
|
||||
use InvalidArgumentException;
|
||||
use DateTimeInterface;
|
||||
use Index\DateTime;
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\IDbStatement;
|
||||
use Index\Data\DbType;
|
||||
use Index\Data\SQLite\SQLiteConnection;
|
||||
|
||||
class DbMigrationManager {
|
||||
public const DEFAULT_TABLE = 'ndx_migrations';
|
||||
|
||||
private const CREATE_TRACK_TABLE = 'CREATE TABLE IF NOT EXISTS %1$s (migration_name %2$s PRIMARY KEY, migration_completed %2$s NOT NULL);';
|
||||
private const CREATE_TRACK_INDEX = 'CREATE INDEX IF NOT EXISTS %1$s_completed_index ON %1$s (migration_completed);';
|
||||
private const DESTROY_TRACK_TABLE = 'DROP TABLE IF EXISTS %s;';
|
||||
|
||||
private const CHECK_STMT = 'SELECT migration_completed IS NOT NULL FROM %s WHERE migration_name = ?;';
|
||||
private const INSERT_STMT = 'INSERT INTO %s (migration_name, migration_completed) VALUES (?, ?);';
|
||||
|
||||
private const TEMPLATE = <<<EOF
|
||||
<?php
|
||||
use Index\Data\IDbConnection;
|
||||
use Index\Data\Migration\IDbMigration;
|
||||
|
||||
final class %s implements IDbMigration {
|
||||
public function migrate(IDbConnection \$conn): void {
|
||||
\$conn->execute('CREATE TABLE ...');
|
||||
}
|
||||
}
|
||||
|
||||
EOF;
|
||||
|
||||
private IDbStatement $checkStmt;
|
||||
private IDbStatement $insertStmt;
|
||||
|
||||
public function __construct(
|
||||
private IDbConnection $conn,
|
||||
private string $tableName = self::DEFAULT_TABLE,
|
||||
) {}
|
||||
|
||||
public function createTrackingTable(): void {
|
||||
// this is not ok but it works for now, there should probably be a generic type bag alias thing
|
||||
$nameType = $this->conn instanceof SQLiteConnection ? 'TEXT' : 'VARCHAR(255)';
|
||||
|
||||
$this->conn->execute(sprintf(self::CREATE_TRACK_TABLE, $this->tableName, $nameType));
|
||||
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
|
||||
}
|
||||
|
||||
public function destroyTrackingTable(): void {
|
||||
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
|
||||
}
|
||||
|
||||
public function prepareStatements(): void {
|
||||
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
|
||||
$this->insertStmt = $this->conn->prepare(sprintf(self::INSERT_STMT, $this->tableName));
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
$this->createTrackingTable();
|
||||
$this->prepareStatements();
|
||||
}
|
||||
|
||||
public function checkMigration(string $name): bool {
|
||||
$this->checkStmt->reset();
|
||||
$this->checkStmt->addParameter(1, $name, DbType::STRING);
|
||||
$this->checkStmt->execute();
|
||||
$result = $this->checkStmt->getResult();
|
||||
return $result->next() && !$result->isNull(0);
|
||||
}
|
||||
|
||||
public function completeMigration(string $name, ?DateTimeInterface $dateTime = null): void {
|
||||
$dateTime = ($dateTime ?? DateTime::utcNow())->format(DateTimeInterface::ATOM);
|
||||
$this->insertStmt->reset();
|
||||
$this->insertStmt->addParameter(1, $name, DbType::STRING);
|
||||
$this->insertStmt->addParameter(2, $dateTime, DbType::STRING);
|
||||
$this->insertStmt->execute();
|
||||
}
|
||||
|
||||
public function template(string $name): string {
|
||||
return sprintf(self::TEMPLATE, $name);
|
||||
}
|
||||
|
||||
public function createFileName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||
$dateTime ??= DateTime::utcNow();
|
||||
|
||||
if(empty($name))
|
||||
throw new InvalidArgumentException('$name may not be empty.');
|
||||
|
||||
$name = str_replace(' ', '_', strtolower($name));
|
||||
if(!preg_match('#^([a-z_]+)$#', $name))
|
||||
throw new InvalidArgumentException('$name may only contain alphabetical, spaces and _ characters.');
|
||||
|
||||
return $dateTime->format('Y_m_d_His_') . trim($name, '_');
|
||||
}
|
||||
|
||||
public function createClassName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||
$dateTime ??= DateTime::utcNow();
|
||||
|
||||
if(empty($name))
|
||||
throw new InvalidArgumentException('$name may not be empty.');
|
||||
|
||||
$name = str_replace(' ', '_', strtolower($name));
|
||||
if(!preg_match('#^([a-z_]+)$#', $name))
|
||||
throw new InvalidArgumentException('$name may only contain alphabetical, spaces and _ characters.');
|
||||
|
||||
$parts = explode('_', trim($name, '_'));
|
||||
$name = '';
|
||||
|
||||
foreach($parts as $part)
|
||||
$name .= ucfirst($part);
|
||||
|
||||
return $name . $dateTime->format('_Ymd_His');
|
||||
}
|
||||
|
||||
public function createNames(string $baseName, ?DateTimeInterface $dateTime = null): object {
|
||||
$dateTime ??= DateTime::utcNow();
|
||||
|
||||
$names = new stdClass;
|
||||
$names->name = $this->createFileName($baseName, $dateTime);
|
||||
$names->className = $this->createClassName($baseName, $dateTime);
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
public function processMigrations(IDbMigrationRepo $migrations): array {
|
||||
$migrations = $migrations->getMigrations();
|
||||
$completed = [];
|
||||
|
||||
foreach($migrations as $migration) {
|
||||
$name = $migration->getName();
|
||||
if($this->checkMigration($name))
|
||||
continue;
|
||||
|
||||
$migration->migrate($this->conn);
|
||||
$this->completeMigration($name);
|
||||
$completed[] = $name;
|
||||
}
|
||||
|
||||
return $completed;
|
||||
}
|
||||
}
|
44
src/Data/Migration/FsDbMigrationInfo.php
Normal file
44
src/Data/Migration/FsDbMigrationInfo.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
// FsDbMigrationInfo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use DateTime;
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
class FsDbMigrationInfo implements IDbMigrationInfo {
|
||||
private string $path;
|
||||
private string $name;
|
||||
private string $className;
|
||||
|
||||
public function __construct(string $path) {
|
||||
$this->path = $path;
|
||||
$this->name = $name = pathinfo($path, PATHINFO_FILENAME);
|
||||
|
||||
$dateTime = substr($name, 0, 17);
|
||||
$dateTime = str_replace('_', '', substr($dateTime, 0, 9)) . substr($dateTime, -8);
|
||||
|
||||
$classParts = explode('_', substr($name, 18));
|
||||
$className = '';
|
||||
|
||||
foreach($classParts as $part)
|
||||
$className .= ucfirst($part);
|
||||
|
||||
$this->className = $className . '_' . $dateTime;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getClassName(): string {
|
||||
return $this->className;
|
||||
}
|
||||
|
||||
public function migrate(IDbConnection $conn): void {
|
||||
require_once $this->path;
|
||||
(new $this->className)->migrate($conn);
|
||||
}
|
||||
}
|
31
src/Data/Migration/FsDbMigrationRepo.php
Normal file
31
src/Data/Migration/FsDbMigrationRepo.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
// FsDbMigrationRepo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
class FsDbMigrationRepo implements IDbMigrationRepo {
|
||||
public function __construct(
|
||||
private string $path
|
||||
) {}
|
||||
|
||||
public function getMigrations(): array {
|
||||
if(!is_dir($this->path))
|
||||
return [];
|
||||
$files = glob(realpath($this->path) . '/*.php');
|
||||
$migrations = [];
|
||||
|
||||
foreach($files as $file)
|
||||
$migrations[] = new FsDbMigrationInfo($file);
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
public function saveMigrationTemplate(string $name, string $body): void {
|
||||
if(!is_dir($this->path))
|
||||
mkdir($this->path, 0777, true);
|
||||
|
||||
file_put_contents(realpath($this->path) . '/' . $name . '.php', $body);
|
||||
}
|
||||
}
|
12
src/Data/Migration/IDbMigration.php
Normal file
12
src/Data/Migration/IDbMigration.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// IDbMigration.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
interface IDbMigration {
|
||||
public function migrate(IDbConnection $conn): void;
|
||||
}
|
14
src/Data/Migration/IDbMigrationInfo.php
Normal file
14
src/Data/Migration/IDbMigrationInfo.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// IDbMigrationInfo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
interface IDbMigrationInfo {
|
||||
public function getName(): string;
|
||||
public function getClassName(): string;
|
||||
public function migrate(IDbConnection $conn): void;
|
||||
}
|
10
src/Data/Migration/IDbMigrationRepo.php
Normal file
10
src/Data/Migration/IDbMigrationRepo.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// IDbMigrationRepo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
interface IDbMigrationRepo {
|
||||
public function getMigrations(): array;
|
||||
}
|
Loading…
Reference in a new issue