diff --git a/VERSION b/VERSION index 4109faf..1b273d1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2301.62020 +0.2301.70411 diff --git a/src/Data/MariaDB/MariaDBConnection.php b/src/Data/MariaDB/MariaDBConnection.php index 15c25e2..0c8d7e6 100644 --- a/src/Data/MariaDB/MariaDBConnection.php +++ b/src/Data/MariaDB/MariaDBConnection.php @@ -1,7 +1,7 @@ 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; } diff --git a/src/Data/Migration/DbMigrationManager.php b/src/Data/Migration/DbMigrationManager.php new file mode 100644 index 0000000..a59c464 --- /dev/null +++ b/src/Data/Migration/DbMigrationManager.php @@ -0,0 +1,148 @@ +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; + } +} diff --git a/src/Data/Migration/FsDbMigrationInfo.php b/src/Data/Migration/FsDbMigrationInfo.php new file mode 100644 index 0000000..21dd6d3 --- /dev/null +++ b/src/Data/Migration/FsDbMigrationInfo.php @@ -0,0 +1,44 @@ +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); + } +} diff --git a/src/Data/Migration/FsDbMigrationRepo.php b/src/Data/Migration/FsDbMigrationRepo.php new file mode 100644 index 0000000..5c21077 --- /dev/null +++ b/src/Data/Migration/FsDbMigrationRepo.php @@ -0,0 +1,31 @@ +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); + } +} diff --git a/src/Data/Migration/IDbMigration.php b/src/Data/Migration/IDbMigration.php new file mode 100644 index 0000000..c9a67c5 --- /dev/null +++ b/src/Data/Migration/IDbMigration.php @@ -0,0 +1,12 @@ +