- Consolidate all 20 API files to use centralized Database helper - Add optimistic locking to ticket updates to prevent concurrent conflicts - Add caching to StatsModel (60s TTL) for dashboard performance - Add health check endpoint (api/health.php) for monitoring - Improve rate limit cleanup with cron script and efficient DirectoryIterator - Enable rate limit response headers (X-RateLimit-*) - Add audit logging for workflow transitions - Log Discord webhook failures instead of silencing - Fix visibility check on export_tickets.php - Add database migration system with performance indexes - Fix cron recurring tickets to use assignTicket method Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
4.4 KiB
PHP
169 lines
4.4 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Database Migration Runner
|
|
*
|
|
* Runs SQL migration files in order. Tracks completed migrations
|
|
* to prevent re-running them.
|
|
*
|
|
* Usage:
|
|
* php migrate.php # Run all pending migrations
|
|
* php migrate.php --status # Show migration status
|
|
* php migrate.php --dry-run # Show what would be run without executing
|
|
*/
|
|
|
|
// Prevent web access
|
|
if (php_sapi_name() !== 'cli') {
|
|
http_response_code(403);
|
|
exit('CLI access only');
|
|
}
|
|
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
|
require_once dirname(__DIR__) . '/helpers/Database.php';
|
|
|
|
$dryRun = in_array('--dry-run', $argv);
|
|
$statusOnly = in_array('--status', $argv);
|
|
|
|
echo "=== Database Migration Runner ===\n\n";
|
|
|
|
try {
|
|
$conn = Database::getConnection();
|
|
} catch (Exception $e) {
|
|
echo "Error: Could not connect to database: " . $e->getMessage() . "\n";
|
|
exit(1);
|
|
}
|
|
|
|
// Create migrations tracking table if it doesn't exist
|
|
$createTable = "CREATE TABLE IF NOT EXISTS migrations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
filename VARCHAR(255) NOT NULL UNIQUE,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_filename (filename)
|
|
)";
|
|
|
|
if (!$conn->query($createTable)) {
|
|
echo "Error: Could not create migrations table: " . $conn->error . "\n";
|
|
exit(1);
|
|
}
|
|
|
|
// Get list of completed migrations
|
|
$completed = [];
|
|
$result = $conn->query("SELECT filename FROM migrations ORDER BY id");
|
|
while ($row = $result->fetch_assoc()) {
|
|
$completed[] = $row['filename'];
|
|
}
|
|
|
|
// Get list of migration files
|
|
$migrationsDir = __DIR__;
|
|
$files = glob($migrationsDir . '/*.sql');
|
|
sort($files);
|
|
|
|
if (empty($files)) {
|
|
echo "No migration files found.\n";
|
|
exit(0);
|
|
}
|
|
|
|
if ($statusOnly) {
|
|
echo "Migration Status:\n";
|
|
echo str_repeat('-', 60) . "\n";
|
|
foreach ($files as $file) {
|
|
$filename = basename($file);
|
|
$status = in_array($filename, $completed) ? '[DONE]' : '[PENDING]';
|
|
echo sprintf(" %s %s\n", $status, $filename);
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
// Find pending migrations
|
|
$pending = [];
|
|
foreach ($files as $file) {
|
|
$filename = basename($file);
|
|
if (!in_array($filename, $completed)) {
|
|
$pending[] = $file;
|
|
}
|
|
}
|
|
|
|
if (empty($pending)) {
|
|
echo "All migrations are up to date.\n";
|
|
exit(0);
|
|
}
|
|
|
|
echo sprintf("Found %d pending migration(s):\n", count($pending));
|
|
foreach ($pending as $file) {
|
|
echo " - " . basename($file) . "\n";
|
|
}
|
|
echo "\n";
|
|
|
|
if ($dryRun) {
|
|
echo "[DRY RUN] No changes made.\n";
|
|
exit(0);
|
|
}
|
|
|
|
// Run pending migrations
|
|
$success = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($pending as $file) {
|
|
$filename = basename($file);
|
|
echo "Running: $filename... ";
|
|
|
|
$sql = file_get_contents($file);
|
|
if ($sql === false) {
|
|
echo "FAILED (could not read file)\n";
|
|
$failed++;
|
|
continue;
|
|
}
|
|
|
|
// Execute migration - handle multiple statements
|
|
$conn->begin_transaction();
|
|
|
|
try {
|
|
// Split by semicolon but respect statements properly
|
|
// Note: This doesn't handle semicolons in strings, but our migrations are simple
|
|
$statements = array_filter(
|
|
array_map('trim', explode(';', $sql)),
|
|
function($stmt) {
|
|
// Remove comments and check if there's actual SQL
|
|
$cleaned = preg_replace('/--.*$/m', '', $stmt);
|
|
return !empty(trim($cleaned));
|
|
}
|
|
);
|
|
|
|
foreach ($statements as $statement) {
|
|
if (!$conn->query($statement)) {
|
|
// Some "errors" are acceptable (like "index already exists")
|
|
$error = $conn->error;
|
|
if (strpos($error, 'Duplicate key name') !== false ||
|
|
strpos($error, 'already exists') !== false) {
|
|
// Index already exists, that's fine
|
|
continue;
|
|
}
|
|
throw new Exception($error);
|
|
}
|
|
}
|
|
|
|
// Record the migration
|
|
$stmt = $conn->prepare("INSERT INTO migrations (filename) VALUES (?)");
|
|
$stmt->bind_param('s', $filename);
|
|
if (!$stmt->execute()) {
|
|
throw new Exception("Could not record migration: " . $conn->error);
|
|
}
|
|
|
|
$conn->commit();
|
|
echo "OK\n";
|
|
$success++;
|
|
|
|
} catch (Exception $e) {
|
|
$conn->rollback();
|
|
echo "FAILED (" . $e->getMessage() . ")\n";
|
|
$failed++;
|
|
}
|
|
}
|
|
|
|
echo "\n";
|
|
echo "=== Migration Complete ===\n";
|
|
echo sprintf(" Success: %d\n", $success);
|
|
echo sprintf(" Failed: %d\n", $failed);
|
|
|
|
exit($failed > 0 ? 1 : 0);
|