MIGRATIONS(7) RSX Framework Manual MIGRATIONS(7) NAME migrations - Database migration system with raw SQL enforcement SYNOPSIS php artisan make:migration:safe php artisan migrate:begin php artisan migrate [--production] php artisan migrate:commit php artisan migrate:rollback DESCRIPTION The RSX framework enforces a forward-only migration strategy using raw SQL statements. Laravel's Schema builder is prohibited to ensure clarity, auditability, and prevent hidden behaviors. PHILOSOPHY 1. Forward-only migrations - No rollbacks, no down() methods 2. Raw SQL only - Direct MySQL statements, no abstractions 3. Fail loud - Migrations must succeed or fail with clear errors 4. Snapshot safety - Development requires database snapshots before migrating MIGRATION RULES Schema Builder Prohibition All migrations MUST use DB::statement() with raw SQL. The following are prohibited: • Schema::create() • Schema::table() • Schema::drop() • Schema::dropIfExists() • Schema::rename() • Blueprint class usage • $table-> method chains The migration validator automatically checks for these patterns and will prevent migrations from running if violations are found. Required Table Structure ALL tables MUST have: id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY This is non-negotiable. Every table needs this exact ID column (SIGNED for easier future migrations). Data Type Standards - What You Need to Know The framework automatically normalizes data types during migration, so you can use simpler types and let the system handle optimization: What You Can Use (System Auto-Converts): • INT → automatically becomes BIGINT • TEXT → automatically becomes LONGTEXT • FLOAT → automatically becomes DOUBLE • Any charset → automatically becomes UTF8MB4 • created_at/updated_at → automatically added with proper defaults • created_by/updated_by → automatically added • deleted_by → automatically added for soft-delete tables What You MUST Be Careful About: • Foreign key columns - Must match the referenced column type exactly Example: If users.id is BIGINT, then orders.user_id must be BIGINT • TINYINT(1) - Preserved for boolean values, won't be converted • Column names ending in _id are assumed to be foreign keys Recommended for Simplicity: • Just use INT for integers (becomes BIGINT automatically) • Just use TEXT for long content (becomes LONGTEXT automatically) • Just use FLOAT for decimals (becomes DOUBLE automatically) • Don't add created_at/updated_at (added automatically) • Don't add created_by/updated_by (added automatically) down() Method Removal The migration system automatically removes down() methods from migration files. Migrations are forward-only - database changes should never be reversed. AUTOMATIC NORMALIZATION What migrate:normalize_schema Does For You After migrations run, the normalize_schema command automatically: 1. Type Conversions: • INT columns → BIGINT (except TINYINT(1) for booleans) • BIGINT UNSIGNED → BIGINT SIGNED • TEXT → LONGTEXT • FLOAT → DOUBLE • All text columns → UTF8MB4 character set 2. Required Columns Added: • created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) • updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE • created_by INT(11) NULL • updated_by INT(11) NULL • deleted_by INT(11) NULL (only for soft-delete tables) 3. Indexes Added: • INDEX on created_at • INDEX on updated_at • INDEX on site_id (for models extending Rsx_Site_Model_Abstract) • INDEX on id+version (for Versionable models) 4. Model-Specific Columns: • site_id BIGINT - for models extending Rsx_Site_Model_Abstract • version INT(11) DEFAULT 1 - for Versionable/Ajaxable models 5. Precision Upgrades: • All DATETIME/TIMESTAMP columns → precision (3) for milliseconds This means you can write simpler migrations and let the system handle the optimization and standardization. The only time you need to be explicit about types is when creating foreign key columns that must match their referenced column exactly. VALIDATION SYSTEM Automatic Validation When running migrations in non-production mode, the system automatically: 1. Validates all pending migrations for Schema builder usage 2. Removes down() methods if present 3. Reports violations with colored output and remediation advice 4. Stops at the first violation to allow correction Validation Output When a violation is detected, you'll see: ❌ Migration Validation Failed File: 2025_09_30_create_example_table.php Line: 28 Violation: Found forbidden Schema builder usage: Schema::create Code Preview: ──────────────────────────────────────── Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); }); ──────────────────────────────────────── Remediation: Use DB::statement("CREATE TABLE...") instead Example: DB::statement('CREATE TABLE users ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), created_at TIMESTAMP NULL DEFAULT NULL )'); Bypassing Validation In production mode (--production flag or APP_ENV=production), validation is skipped. This should only be used when absolutely necessary. MIGRATION WORKFLOW Development Workflow 1. Create snapshot: php artisan migrate:begin 2. Create migration: php artisan make:migration:safe 3. Write migration using raw SQL 4. Run migrations: php artisan migrate 5. If successful: php artisan migrate:commit 6. If failed: System auto-rollbacks to snapshot Production Workflow 1. Create migration: php artisan make:migration:safe 2. Write migration using raw SQL 3. Test thoroughly in development/staging 4. Run migrations: php artisan migrate --production Note: No snapshot protection in production mode. Ensure migrations are thoroughly tested before running in production. MIGRATION EXAMPLES Creating a Table (Simple Version - Recommended) public function up() { // You can write this simple version - system auto-normalizes types DB::statement(" CREATE TABLE products ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -- becomes BIGINT name VARCHAR(255) NOT NULL, description TEXT, -- becomes LONGTEXT price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock_quantity INT NOT NULL DEFAULT 0, -- becomes BIGINT is_active TINYINT(1) NOT NULL DEFAULT 1, -- stays TINYINT(1) category_id INT NULL, -- becomes BIGINT INDEX idx_category (category_id), INDEX idx_active (is_active) -- No need for created_at/updated_at - added automatically ) "); } Creating a Table (Explicit Version - If You Prefer) public function up() { // Or be explicit about types if you prefer DB::statement(" CREATE TABLE products ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description LONGTEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock_quantity BIGINT NOT NULL DEFAULT 0, is_active TINYINT(1) NOT NULL DEFAULT 1, category_id BIGINT NULL, created_at TIMESTAMP NULL DEFAULT NULL, updated_at TIMESTAMP NULL DEFAULT NULL, INDEX idx_category (category_id), INDEX idx_active (is_active), INDEX idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); } Adding Columns public function up() { DB::statement("ALTER TABLE users ADD COLUMN age BIGINT NULL AFTER email"); DB::statement("ALTER TABLE users ADD INDEX idx_age (age)"); } Modifying Columns public function up() { // Change column type DB::statement("ALTER TABLE products MODIFY COLUMN price DECIMAL(12,2)"); // Rename column DB::statement("ALTER TABLE users CHANGE COLUMN username user_name VARCHAR(100)"); // Add default value DB::statement("ALTER TABLE posts ALTER COLUMN status SET DEFAULT 'draft'"); } Managing Indexes public function up() { // Add index DB::statement("CREATE INDEX idx_email ON users (email)"); // Add unique index DB::statement("CREATE UNIQUE INDEX idx_unique_slug ON posts (slug)"); // Add composite index DB::statement("CREATE INDEX idx_user_status ON orders (user_id, status)"); // Drop index DB::statement("DROP INDEX idx_old_index ON table_name"); } Foreign Keys (IMPORTANT - Match Types Exactly) public function up() { // CRITICAL: Foreign key columns must match referenced column type // If users.id is BIGINT, orders.user_id must also be BIGINT // First, ensure the column has correct type (if not already created) DB::statement("ALTER TABLE orders ADD COLUMN user_id BIGINT NULL"); // Then add the foreign key constraint DB::statement(" ALTER TABLE orders ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE "); // To drop a foreign key DB::statement("ALTER TABLE orders DROP FOREIGN KEY orders_user_fk"); } // Note: After normalization, all id columns are BIGINT, so foreign keys // should always use BIGINT to avoid type mismatches Data Migrations public function up() { // Simple update DB::statement("UPDATE users SET role = 'member' WHERE role IS NULL"); // Complex migration with temporary column DB::statement("ALTER TABLE orders ADD COLUMN total_new DECIMAL(10,2)"); DB::statement("UPDATE orders SET total_new = quantity * price"); DB::statement("ALTER TABLE orders DROP COLUMN total"); DB::statement("ALTER TABLE orders CHANGE total_new total DECIMAL(10,2)"); } ERROR MESSAGES "Migration validation failed: Schema builder usage detected" Your migration uses Laravel's Schema builder. Rewrite using DB::statement() with raw SQL. "Migration mode not active!" You're in development mode and haven't created a snapshot. Run: php artisan migrate:begin "Unauthorized migrations detected!" Migration files exist that weren't created via make:migration:safe. Recreate them using the proper command. SECURITY CONSIDERATIONS SQL Injection When using dynamic values in migrations, always use parameter binding: ✅ CORRECT: DB::statement("UPDATE users SET status = ? WHERE created_at < ?", ['active', '2025-01-01']); ❌ WRONG: DB::statement("UPDATE users SET status = '$status' WHERE created_at < '$date'"); Production Safety • Always test migrations in development/staging first • Keep migrations small and focused • Never reference models or services in migrations • Migrations must be self-contained and idempotent where possible DEBUGGING Viewing Pending Migrations php artisan migrate:status Testing a Migration 1. Create snapshot: php artisan migrate:begin 2. Run migration: php artisan migrate 3. If it fails, automatic rollback occurs 4. Fix the migration file 5. Try again: php artisan migrate Common Issues • "Class not found" - Don't reference models in migrations • "Syntax error" - Check your SQL syntax, test in MySQL client first • "Foreign key constraint" - Ensure referenced table/column exists • "Duplicate column" - Check if column already exists before adding SEE ALSO rsx:man database - Database system overview rsx:man coding_standards - General coding standards rsx:man error_handling - Error handling patterns AUTHORS RSX Framework Team RSX Framework September 2025 MIGRATIONS(7)