- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Writing Laravel Migrations That Are Safe to Run Twice and Easy to Roll Back
About Post
The deploy runs php artisan migrate. The migration adds two columns and a unique index. The first column is created. The second column is created. The index fails, because of a duplicate value nobody knew was there.
You fix the data and run the migration again. It fails immediately: column already exists.
Welcome to one of MySQL's less famous features, and the reason "run it again" is not a recovery plan unless you designed your migrations for it.
Why a failed migration leaves a mess
In MySQL, schema changes like ALTER TABLE and CREATE INDEX cause an implicit commit. They can't be rolled back inside a transaction. So when a migration fails halfway, everything before the failure stays applied.
Meanwhile, Laravel only records a migration in the migrations table after it finishes successfully. The result is the worst of both worlds: the database is half-changed, and Laravel thinks the migration never ran. The next attempt replays the steps that already happened, and trips over them.
PostgreSQL supports transactional DDL, so a failed migration there can be rolled back cleanly. On MySQL and MariaDB, which plenty of Laravel apps run on, you have to plan for partial failure yourself. Here are the five habits that make that easy.
1. One concern per migration
The simplest protection is to make each migration small. A migration that adds one column can only fail in one place. A migration that adds three columns, renames a fourth, creates an index and updates data can leave you in a dozen different half-states.
Small migrations also roll back more precisely, and their names (add_due_date_to_invoices_table) tell you exactly what happened in a deploy log.
2. Guard the steps that can't be repeated
For changes that would fail if repeated, check before you act. The schema builder can tell you what exists:
return new class extends Migration
{
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
if (! Schema::hasColumn('invoices', 'due_date')) {
$table->date('due_date')->nullable()->after('issued_at');
}
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
if (Schema::hasColumn('invoices', 'due_date')) {
$table->dropColumn('due_date');
}
});
}
};
Now a retry after a partial failure skips what's already done and carries on. Schema::hasTable() and Schema::hasIndex() cover tables and indexes the same way.
One honest caveat: guards can hide drift. If due_date already exists but as a string instead of a date, the guard happily skips it. Use guards as a safety net for retries, not as a way to paper over databases that have wandered away from your migrations.
3. Write a real down(), and be honest when you can't
A down() method should undo exactly what up() did, in reverse order. If up() adds a column and then an index on it, down() drops the index and then the column.
Some migrations aren't truly reversible. If up() drops a column, down() can recreate the column, but not the data that was in it. Don't pretend otherwise. Either keep the data somewhere first, or make the irreversibility explicit:
public function down(): void
{
throw new RuntimeException('Irreversible: legacy_notes data was removed.');
}
A loud failure on rollback is much better than a silent "rollback" that leaves you believing the data is back.
4. Keep data changes out of schema migrations
It's tempting to backfill the new column right inside the migration. Resist it, for three reasons:
- Time. Updating a large table inside a deploy can hold locks and blow past your deploy timeout.
- Models change. A migration that uses
Invoice::query()runs today's model code. A year from now, someone runsmigrate:freshand the model has new casts, scopes or a renamed column, and an old migration breaks. - Retries. A data update that fails halfway needs to resume, not restart.
Put the backfill in its own Artisan command (or a queued job) that's safe to run any number of times:
public function handle(): void
{
DB::table('invoices')
->whereNull('due_date')
->chunkById(500, function ($invoices) {
foreach ($invoices as $invoice) {
DB::table('invoices')->where('id', $invoice->id)->update([
'due_date' => Carbon::parse($invoice->issued_at)->addDays(30),
]);
}
});
}
The whereNull('due_date') filter is what makes it idempotent: run it twice and the second run finds nothing to do. Stop it halfway and the next run picks up where it left off. It uses the query builder, not the model, so it won't break when the model changes. And chunkById is the right choice when you're updating the same column you filter on.
The rule: schema migrations change the shape of the database; commands change the data. Each should be safe to run again after a failure.
5. Test the round trip in CI
Most broken down() methods are discovered at the worst possible moment: when you actually need them. Test them on every pull request instead:
php artisan migrate:fresh --force
php artisan migrate:rollback --force
php artisan migrate --force
After migrate:fresh, every migration is in the same batch, so migrate:rollback runs every down() method. Then migrate runs every up() again on top of the rolled-back schema. If any step fails, the build fails. Run this against the same database engine as production, not SQLite, or you'll test a different set of behaviours.
Two more checks worth knowing: php artisan migrate --pretend prints the SQL without running it, which is great for reviewing a risky migration, and nothing beats running new migrations against a recent copy of production data before the real deploy.
A word on rolling back in production
Here's my opinion: down() methods are essential in development and CI, and useful for a quick revert right after a deploy. But once real data has been written to the new structure, rolling back is often riskier than rolling forward with a new, fixing migration. Write good down() methods anyway. Just don't make them your only plan.
Checklist
- One concern per migration.
- Guards on steps that can't be repeated, with drift in mind.
- A
down()that mirrorsup(), or an explicit "irreversible". - Data changes in idempotent commands, using the query builder.
- CI runs migrate, rollback and migrate again on the production engine.
Which one has saved you, or would have? I suspect for most of us it's number four.

Be first to comment it...