Database Migrations
Database migrations provide a version control system for your database schema, allowing you to track, manage, and apply incremental changes to your database structure over time.
Migrations are essential for team development and deployment workflows. They ensure that database changes are tracked in version control, can be reviewed before application, and can be consistently applied across development, staging, and production environments.
Migration Types
Fuwafuwa Framework supports two types of migrations:
1. SQL Migrations
SQL migrations are raw SQL files that are executed directly against the database. They are the simplest form of migration and work well for straightforward schema changes.
| File Suffix | Database | Example |
|---|---|---|
_sqlite.sql |
SQLite | 001_create_users_sqlite.sql |
.sql |
MySQL | 001_create_password_reset_tokens.sql |
2. PHP Class Migrations
PHP class migrations provide more flexibility, allowing you to use the framework's database abstraction and perform complex logic during migrations.
<?php
// migrations/migration_add_notifications_table.php
class migration_add_notifications_table {
use \Fuwafuwa\Traits\SQL;
public function up() {
$this->SQL("
CREATE TABLE IF NOT EXISTS notification (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_login TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'info',
title TEXT NOT NULL,
message TEXT NOT NULL,
is_read INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$this->SQL("CREATE INDEX idx_notification_user ON notification(user_login)");
}
public function down() {
$this->SQL("DROP INDEX IF EXISTS idx_notification_user");
$this->SQL("DROP TABLE IF EXISTS notification");
}
}
New Project Setup
For new projects, use the complete schema initialization files which contain all tables, indexes, and sample data:
SQLite Setup
# Create new SQLite database
sqlite3 app/db/fuwafuwa.db < sqlite_init.sql
# Or with MySQL
mysql -u username -p database_name < mysql_init.sql
The sqlite_init.sql and mysql_init.sql files are always kept up-to-date
with the current database schema. For new installations, use these files directly instead of
running individual migrations.
Creating Migrations (For Existing Projects)
If you need to modify an existing database after initial setup, create migration files
in the migrations/ directory:
SQL Migration Best Practices
-- migrations/001_add_categories_sqlite.sql
-- Migration: Create categories table
-- Description: Adds product categories for inventory management
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
parent_id INTEGER DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
);
-- Create indexes for common queries
CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id);
CREATE INDEX IF NOT EXISTS idx_categories_slug ON categories(slug);
- Use sequential numbering:
001_,002_, etc. - Use descriptive names:
create_users,add_email_index - Include database suffix:
_sqlite.sqlor_mysql.sql
Running Migrations
Manual Execution
For SQLite databases, run migrations directly:
# Run a single migration (example)
sqlite3 app/db/fuwafuwa.db < migrations/001_your_migration_sqlite.sql
# Run all SQLite migrations in order
for f in migrations/*_sqlite.sql; do
echo "Running $f..."
sqlite3 app/db/fuwafuwa.db < "$f"
done
Using PHP Migrations
For PHP class migrations, include and execute them:
<?php
// Include the migration
require_once 'migrations/migration_add_notifications_table.php';
// Run the migration
$migration = new migration_add_notifications_table();
$migration->up();
Database Initialization
For new installations, use the complete schema files:
# Initialize SQLite database
cat sqlite_init.sql | sqlite3 app/db/fuwafuwa.db
# Or with MySQL
mysql -u username -p database_name < mysql_init.sql
Rollback Migrations
SQL migrations should include rollback statements as comments:
-- Migration: Add email verification column
-- Rollback: ALTER TABLE users DROP COLUMN email_verified_at;
ALTER TABLE users ADD COLUMN email_verified_at DATETIME DEFAULT NULL;
CREATE INDEX idx_users_verified ON users(email_verified_at);
PHP class migrations implement the down() method for rollbacks:
public function down() {
// Reverse the changes made in up()
$this->SQL("DROP INDEX IF EXISTS idx_users_verified");
$this->SQL("ALTER TABLE users DROP COLUMN email_verified_at");
}
Schema Files
The framework includes complete schema files for fresh installations:
| File | Database | Purpose |
|---|---|---|
sqlite_init.sql |
SQLite | Complete schema with sample data |
mysql_init.sql |
MySQL | Complete schema for MySQL |
Never modify sqlite_init.sql or mysql_init.sql after release.
Use migrations for subsequent schema changes to preserve existing data.
Best Practices
- Use IF EXISTS / IF NOT EXISTS - Makes migrations idempotent and safer to run
- Create indexes separately - After table creation for better performance
- Test migrations - Run on a copy of production data before applying
- Keep migrations small - One logical change per migration file
- Document rollbacks - Always include how to reverse the change
- Use transactions - When possible, wrap changes in transactions
- Version control - Always commit migrations to version control
Example: Safe Migration Pattern
-- migrations/003_add_user_profile_sqlite.sql
-- Step 1: Create new table
CREATE TABLE IF NOT EXISTS user_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_login TEXT NOT NULL UNIQUE,
avatar TEXT,
bio TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_login) REFERENCES user(login) ON DELETE CASCADE
);
-- Step 2: Create indexes
CREATE INDEX IF NOT EXISTS idx_user_profiles_login ON user_profiles(user_login);
-- Step 3: Migrate data (if needed)
-- INSERT INTO user_profiles (user_login) SELECT login FROM user;
-- Rollback:
-- DROP TABLE IF EXISTS user_profiles;
Environment-Specific Setup
Development
# Reset database during development
rm app/db/fuwafuwa.db
sqlite3 app/db/fuwafuwa.db < sqlite_init.sql
# Apply new migrations
sqlite3 app/db/fuwafuwa.db < migrations/00X_your_migration_sqlite.sql
Testing
# Create test database
cp app/db/fuwafuwa.db app/db/test.db
# Or fresh test database
sqlite3 app/db/test.db < sqlite_init.sql
Production
Always backup before applying migrations:
# Create backup first
cp app/db/fuwafuwa.db "app/db/backup_$(date +%Y%m%d_%H%M%S).db"
# Apply migration
sqlite3 app/db/fuwafuwa.db < migrations/00X_migration_sqlite.sql