Add performance, security, and reliability improvements
- 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>
This commit is contained in:
168
migrations/migrate.php
Normal file
168
migrations/migrate.php
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/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);
|
||||
Reference in New Issue
Block a user