#!/usr/bin/env php getModules(); $hasModules = count($modules) > 0; if (!$hasModules) { fwrite(STDOUT, "module-migrate: no modules enabled, nothing to do.\n"); } else { // Ensure tracking table exists (safe if already created by db/updates/ script) DB::query( 'CREATE TABLE IF NOT EXISTS `module_migrations` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `module_id` VARCHAR(64) NOT NULL, `filename` VARCHAR(255) NOT NULL, `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uq_module_migration` (`module_id`, `filename`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' ); } foreach ($modules as $manifest) { $migrationsPath = $manifest->migrationsPath; if ($migrationsPath === null || !is_dir($migrationsPath)) { continue; } // Discover SQL files $files = glob($migrationsPath . '/*.sql'); if ($files === false || count($files) === 0) { continue; } sort($files); // alphabetical order // Get already-applied filenames $applied = []; $rows = DB::select('SELECT filename FROM module_migrations WHERE module_id = ?', [$manifest->id]); foreach ($rows as $row) { $applied[$row['filename']] = true; } foreach ($files as $file) { $filename = basename($file); if (isset($applied[$filename])) { continue; } $sql = file_get_contents($file); if ($sql === false || trim($sql) === '') { fwrite(STDERR, sprintf("module-migrate: WARNING skipping empty file %s/%s\n", $manifest->id, $filename)); continue; } fwrite(STDOUT, sprintf("module-migrate: applying %s/%s ... ", $manifest->id, $filename)); // Execute migration inside a transaction so partial failures don't leave the DB inconsistent DB::query('START TRANSACTION'); try { $statements = array_filter(array_map('trim', explode(';', $sql)), static fn (string $s): bool => $s !== ''); foreach ($statements as $statement) { DB::query($statement); } // Record as applied DB::query( 'INSERT INTO module_migrations (module_id, filename) VALUES (?, ?)', [$manifest->id, $filename] ); DB::query('COMMIT'); } catch (\Throwable $migrationError) { DB::query('ROLLBACK'); throw new \RuntimeException( sprintf("Migration %s/%s failed: %s", $manifest->id, $filename, $migrationError->getMessage()), 0, $migrationError ); } fwrite(STDOUT, "OK\n"); $totalApplied++; } } if (!$hasModules) { // Already reported above. } elseif ($totalApplied === 0) { fwrite(STDOUT, "module-migrate: all modules up to date, 0 new migrations.\n"); } else { fwrite(STDOUT, sprintf("module-migrate: applied %d migration(s).\n", $totalApplied)); } } catch (Throwable $exception) { fwrite(STDERR, sprintf("module-migrate: FAILED: %s\n", $exception->getMessage())); $exitCode = 1; } $vendorWarningsIgnored = moduleCliVendorWarningsIgnoredSince($warningsBefore); fwrite(STDOUT, sprintf("module-migrate: vendor_warnings_ignored=%d\n", $vendorWarningsIgnored)); return $exitCode; } if (PHP_SAPI === 'cli' && realpath((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) { exit(moduleMigrateRun()); }