} */ public function applyPendingMigrations(): array { $this->repository->ensureTrackingTable(); $files = $this->discoverFiles(); $applied = $this->repository->getAppliedFilenames(); $totalApplied = 0; $skippedEmpty = 0; $appliedFilenames = []; foreach ($files as $file) { $filename = basename($file); if (isset($applied[$filename])) { continue; } $sql = file_get_contents($file); if ($sql === false || trim($sql) === '') { $skippedEmpty++; continue; } $this->repository->beginTransaction(); try { $statements = SqlStatementParser::splitStatements($sql); foreach ($statements as $statement) { $this->repository->executeStatement($statement); } $this->repository->recordApplied($filename); $this->repository->commit(); } catch (\Throwable $migrationError) { $this->repository->rollback(); throw new RuntimeException( sprintf("Core migration %s failed: %s", $filename, $migrationError->getMessage()), 0, $migrationError ); } $totalApplied++; $appliedFilenames[] = $filename; } return [ 'ok' => true, 'applied' => $totalApplied, 'skipped_empty' => $skippedEmpty, 'total_files' => count($files), 'applied_filenames' => $appliedFilenames, ]; } /** * Inspect applied vs. pending migrations without running anything. * * @return array{applied: list, pending: list} */ public function status(): array { $this->repository->ensureTrackingTable(); $files = $this->discoverFiles(); $appliedMap = $this->repository->getAppliedFilenames(); $applied = []; $pending = []; foreach ($files as $file) { $filename = basename($file); if (isset($appliedMap[$filename])) { $applied[] = $filename; } else { $pending[] = $filename; } } sort($applied); sort($pending); return ['applied' => $applied, 'pending' => $pending]; } /** * @return list Absolute paths of `*.sql` files in updates dir, sorted alphabetically. */ private function discoverFiles(): array { if (!is_dir($this->updatesPath)) { return []; } $files = glob($this->updatesPath . '/*.sql'); if ($files === false || count($files) === 0) { return []; } sort($files); return $files; } }