Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | n/a |
0 / 0 |
n/a |
0 / 0 |
CRAP | n/a |
0 / 0 |
|||
| Db | n/a |
0 / 0 |
n/a |
0 / 0 |
39 | n/a |
0 / 0 |
|||
| startExecuteSqlFile | n/a |
0 / 0 |
n/a |
0 / 0 |
7 | |||||
| executeSql | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
| startUsingDatabase | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
| startTestDataImport | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
| startConfigDataImport | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
| startMigrations | n/a |
0 / 0 |
n/a |
0 / 0 |
4 | |||||
| executeTestData | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| openSqlFileHandle | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| readDelimiterDirective | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| lineEndsWithStatementDelimiter | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| filterMigrationFilesByPhase | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
| extractSqlStatement | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| executeSqlStatement | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
| applyDatabaseNameToConnection | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| resolveMigrationFileList | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
| 1 | <?php |
| 2 | |
| 3 | namespace BO\Zmsbackend\Cli; |
| 4 | |
| 5 | /** |
| 6 | * @codeCoverageIgnore |
| 7 | * @SuppressWarnings(Short) |
| 8 | */ |
| 9 | class Db |
| 10 | { |
| 11 | public static $baseDSN = ''; |
| 12 | |
| 13 | public static function startExecuteSqlFile($file, $databaseName = null, $verbose = true) |
| 14 | { |
| 15 | $databaseConnection = self::startUsingDatabase($databaseName, $verbose); |
| 16 | $startedAt = microtime(true); |
| 17 | $sqlFileHandle = self::openSqlFileHandle($file); |
| 18 | |
| 19 | if ($verbose) { |
| 20 | \App::$log->info('Importing SQL file', ['file' => basename($file)]); |
| 21 | } |
| 22 | |
| 23 | $statementDelimiter = ';'; |
| 24 | $statementBuffer = ''; |
| 25 | |
| 26 | while ($line = $sqlFileHandle['readLine']($sqlFileHandle['handle'])) { |
| 27 | $delimiterFromLine = self::readDelimiterDirective($line); |
| 28 | if ($delimiterFromLine !== null) { |
| 29 | $statementDelimiter = $delimiterFromLine; |
| 30 | continue; |
| 31 | } |
| 32 | |
| 33 | $statementBuffer .= $line; |
| 34 | if (!self::lineEndsWithStatementDelimiter($line, $statementDelimiter)) { |
| 35 | continue; |
| 36 | } |
| 37 | |
| 38 | $sqlStatement = self::extractSqlStatement($statementBuffer, $statementDelimiter); |
| 39 | $statementBuffer = ''; |
| 40 | |
| 41 | if ($sqlStatement === '') { |
| 42 | continue; |
| 43 | } |
| 44 | |
| 45 | self::executeSqlStatement($databaseConnection, $sqlStatement, $file, $verbose); |
| 46 | } |
| 47 | |
| 48 | $sqlFileHandle['close']($sqlFileHandle['handle']); |
| 49 | |
| 50 | if ($verbose) { |
| 51 | \App::$log->info('SQL import finished', [ |
| 52 | 'file' => basename($file), |
| 53 | 'seconds' => round(microtime(true) - $startedAt, 3), |
| 54 | ]); |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | public static function executeSql($query, $databaseName = null) |
| 59 | { |
| 60 | $databaseConnection = self::startUsingDatabase($databaseName, false); |
| 61 | $databaseConnection->exec($query); |
| 62 | } |
| 63 | |
| 64 | public static function startUsingDatabase($databaseName = null, $verbose = true): \BO\Zmsbackend\Connection\Pdo |
| 65 | { |
| 66 | if (!self::$baseDSN) { |
| 67 | self::$baseDSN = \BO\Zmsbackend\Connection\Select::$writeSourceName; |
| 68 | } |
| 69 | |
| 70 | \BO\Zmsbackend\Connection\Select::closeWriteConnection(); |
| 71 | self::applyDatabaseNameToConnection($databaseName); |
| 72 | |
| 73 | if ($verbose) { |
| 74 | \App::$log->info('Using database connection', [ |
| 75 | 'dsn' => \BO\Zmsbackend\Connection\Select::$writeSourceName, |
| 76 | ]); |
| 77 | } |
| 78 | |
| 79 | return \BO\Zmsbackend\Connection\Select::getWriteConnection(); |
| 80 | } |
| 81 | |
| 82 | public static function startTestDataImport($fixturesDirectory, $filename = 'mysql_zmsbo.sql') |
| 83 | { |
| 84 | $defaultDatabaseName =& \BO\Zmsbackend\Connection\Select::$dbname_zms; |
| 85 | |
| 86 | $databaseConnection = self::startUsingDatabase('information_schema'); |
| 87 | $databaseConnection->exec("DROP DATABASE IF EXISTS `$defaultDatabaseName`;"); |
| 88 | $databaseConnection->exec("CREATE DATABASE IF NOT EXISTS `$defaultDatabaseName`;"); |
| 89 | |
| 90 | self::startExecuteSqlFile($fixturesDirectory . '/' . $filename); |
| 91 | } |
| 92 | |
| 93 | public static function startConfigDataImport() |
| 94 | { |
| 95 | $defaults = new \BO\Zmsentities\Config(); |
| 96 | (new \BO\Zmsbackend\Config\Service\Config())->updateEntity($defaults); |
| 97 | } |
| 98 | |
| 99 | public static function startMigrations($migrationList, $commit = true, ?string $phase = null) |
| 100 | { |
| 101 | $migrationFiles = self::resolveMigrationFileList($migrationList); |
| 102 | $migrationFiles = self::filterMigrationFilesByPhase($migrationFiles, $phase); |
| 103 | $databaseConnection = self::startUsingDatabase(); |
| 104 | $completedMigrations = $databaseConnection->fetchPairs( |
| 105 | 'SELECT filename, changeTimestamp FROM migrations' |
| 106 | ); |
| 107 | $addedMigrationCount = 0; |
| 108 | |
| 109 | foreach ($migrationFiles as $migrationFile) { |
| 110 | $migrationFilename = basename($migrationFile); |
| 111 | if (array_key_exists($migrationFilename, $completedMigrations)) { |
| 112 | continue; |
| 113 | } |
| 114 | |
| 115 | $addedMigrationCount++; |
| 116 | if (!$commit) { |
| 117 | \App::$log->info('Pending migration', [ |
| 118 | 'index' => $addedMigrationCount, |
| 119 | 'migration' => $migrationFilename, |
| 120 | ]); |
| 121 | continue; |
| 122 | } |
| 123 | |
| 124 | self::startExecuteSqlFile($migrationFile); |
| 125 | $databaseConnection->prepare('INSERT INTO `migrations` SET `filename` = :filename') |
| 126 | ->execute(['filename' => $migrationFilename]); |
| 127 | } |
| 128 | |
| 129 | \App::$log->info('Migration check finished', [ |
| 130 | 'completed' => count($completedMigrations), |
| 131 | 'added' => $addedMigrationCount, |
| 132 | ]); |
| 133 | |
| 134 | return $addedMigrationCount; |
| 135 | } |
| 136 | |
| 137 | public static function executeTestData(string $testName, string $step) |
| 138 | { |
| 139 | $fixturesDirectory = realpath(__DIR__ . '/../../../tests/Zmsbackend/Service/fixtures/'); |
| 140 | $sqlFile = $fixturesDirectory . '/' . $testName . '/' . $step . '.sql'; |
| 141 | |
| 142 | if (!file_exists($sqlFile)) { |
| 143 | return; |
| 144 | } |
| 145 | |
| 146 | self::startExecuteSqlFile($sqlFile, null, false); |
| 147 | } |
| 148 | |
| 149 | private static function openSqlFileHandle(string $file): array |
| 150 | { |
| 151 | $isGzipCompressed = substr($file, -3) === '.gz'; |
| 152 | |
| 153 | if ($isGzipCompressed) { |
| 154 | return [ |
| 155 | 'handle' => gzopen($file, 'r'), |
| 156 | 'readLine' => 'gzgets', |
| 157 | 'close' => 'gzclose', |
| 158 | ]; |
| 159 | } |
| 160 | |
| 161 | return [ |
| 162 | 'handle' => fopen($file, 'r'), |
| 163 | 'readLine' => 'fgets', |
| 164 | 'close' => 'fclose', |
| 165 | ]; |
| 166 | } |
| 167 | |
| 168 | private static function readDelimiterDirective(string $line): ?string |
| 169 | { |
| 170 | if (!preg_match('/^\s*DELIMITER\s+(\S+)\s*$/i', rtrim($line), $matches)) { |
| 171 | return null; |
| 172 | } |
| 173 | |
| 174 | return $matches[1]; |
| 175 | } |
| 176 | |
| 177 | private static function lineEndsWithStatementDelimiter(string $line, string $statementDelimiter): bool |
| 178 | { |
| 179 | if ($statementDelimiter === ';') { |
| 180 | return (bool) preg_match('/;\s*$/', $line); |
| 181 | } |
| 182 | |
| 183 | return (bool) preg_match( |
| 184 | '/' . preg_quote($statementDelimiter, '/') . '\s*$/', |
| 185 | rtrim($line) |
| 186 | ); |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Split migrations into expand/contract phases by filename for zero-downtime |
| 191 | * (Expand–Contract) deployments. |
| 192 | * |
| 193 | * - contract phase: files whose name contains "-contract-" or "-contract." (destructive cleanup) |
| 194 | * - expand phase: every other file, i.e. "*-expand-*" plus unprefixed additive migrations |
| 195 | * - no phase (null/empty): all files, preserving the legacy behaviour |
| 196 | */ |
| 197 | private static function filterMigrationFilesByPhase(array $migrationFiles, ?string $phase): array |
| 198 | { |
| 199 | if ($phase === null || $phase === '') { |
| 200 | return $migrationFiles; |
| 201 | } |
| 202 | |
| 203 | $isContract = static function (string $migrationFile): bool { |
| 204 | return (bool) preg_match('/-contract[-.]/', basename($migrationFile)); |
| 205 | }; |
| 206 | |
| 207 | if ($phase === 'contract') { |
| 208 | return array_values(array_filter($migrationFiles, $isContract)); |
| 209 | } |
| 210 | |
| 211 | if ($phase === 'expand') { |
| 212 | return array_values(array_filter( |
| 213 | $migrationFiles, |
| 214 | static fn(string $migrationFile): bool => !$isContract($migrationFile) |
| 215 | )); |
| 216 | } |
| 217 | |
| 218 | throw new \InvalidArgumentException( |
| 219 | "Unknown migration phase '$phase'; expected 'expand' or 'contract'" |
| 220 | ); |
| 221 | } |
| 222 | |
| 223 | private static function extractSqlStatement(string $statementBuffer, string $statementDelimiter): string |
| 224 | { |
| 225 | $sqlStatement = $statementBuffer; |
| 226 | |
| 227 | if ($statementDelimiter !== ';') { |
| 228 | $sqlStatement = preg_replace( |
| 229 | '/' . preg_quote($statementDelimiter, '/') . '\s*$/', |
| 230 | '', |
| 231 | $sqlStatement |
| 232 | ); |
| 233 | } |
| 234 | |
| 235 | return trim($sqlStatement); |
| 236 | } |
| 237 | |
| 238 | private static function executeSqlStatement( |
| 239 | \BO\Zmsbackend\Connection\Pdo $databaseConnection, |
| 240 | string $sqlStatement, |
| 241 | string $sourceFile, |
| 242 | bool $verbose |
| 243 | ): void { |
| 244 | try { |
| 245 | $databaseConnection->exec($sqlStatement); |
| 246 | } catch (\Exception $exception) { |
| 247 | if ($verbose) { |
| 248 | \App::$log->error('SQL import failed', [ |
| 249 | 'file' => basename($sourceFile), |
| 250 | 'method' => __METHOD__, |
| 251 | 'exception' => get_class($exception), |
| 252 | 'message' => $exception->getMessage(), |
| 253 | 'code' => $exception->getCode(), |
| 254 | ]); |
| 255 | } |
| 256 | |
| 257 | throw $exception; |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | private static function applyDatabaseNameToConnection(?string $databaseName): void |
| 262 | { |
| 263 | if ($databaseName === null) { |
| 264 | \BO\Zmsbackend\Connection\Select::$writeSourceName = self::$baseDSN; |
| 265 | return; |
| 266 | } |
| 267 | |
| 268 | $defaultDatabaseName =& \BO\Zmsbackend\Connection\Select::$dbname_zms; |
| 269 | \BO\Zmsbackend\Connection\Select::$writeSourceName = preg_replace( |
| 270 | "#dbname=$defaultDatabaseName.*?;#", |
| 271 | "dbname=$databaseName;", |
| 272 | self::$baseDSN |
| 273 | ); |
| 274 | } |
| 275 | |
| 276 | private static function resolveMigrationFileList($migrationList): array |
| 277 | { |
| 278 | if (!is_array($migrationList)) { |
| 279 | $migrationList = glob($migrationList . '/*.sql'); |
| 280 | } |
| 281 | |
| 282 | sort($migrationList); |
| 283 | |
| 284 | return $migrationList; |
| 285 | } |
| 286 | } |