Added database migration utility.

This commit is contained in:
flash 2023-01-07 04:12:05 +00:00
parent f8c6602ab9
commit fbe4fe18de
8 changed files with 277 additions and 6 deletions

View File

@ -1 +1 @@
0.2301.62020
0.2301.70411

View File

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

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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;
}

View 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;
}

View 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;
}