Migrations
- Introduction
- Generating Migrations
- Migration Structure
- Running Migrations
- Creating Tables
- Modifying Tables
- Dropping Tables
- Checking Schema
Introduction
Migrations are like version control for your database, allowing your team to define and share the application's database schema definition. If you have ever had to tell a teammate to manually add a column to their local database schema, you've faced the problem that database migrations solve.
Generating Migrations
Use the make:migration command to generate a migration:
dart run magic:magic make:migration create_users_table
dart run magic:magic make:migration CreateUsersTable # PascalCase also works
dart run magic:magic make:migration add_avatar_to_users
This creates a file in lib/database/migrations/ with:
- Timestamp-prefixed filename (e.g.,
m_2024_01_15_120000_create_users_table.dart) - Migration class with
upanddownmethods - Proper imports
[!NOTE] Migrations starting with
create_and ending with_tableautomatically use a special stub withSchema.create()boilerplate.
Migration Structure
A migration class contains two methods: up and down. The up method adds new tables, columns, or indexes, while the down method should reverse the operations:
import 'package:magic/magic.dart';
class CreateUsersTable extends Migration {
@override
String get name => '2024_01_15_120000_create_users_table';
@override
void up() {
Schema.create('users', (Blueprint table) {
table.id();
table.string('name');
table.string('email').unique();
table.string('password');
table.boolean('is_active').defaultValue(true);
table.timestamps();
});
}
@override
void down() {
Schema.dropIfExists('users');
}
}
Migration Naming Convention
Use timestamp prefixes for proper ordering: YYYY_MM_DD_HHMMSS_description
2024_01_15_120000_create_users_table2024_01_15_120001_add_avatar_to_users2024_01_15_120002_rename_name_to_full_name
Running Migrations
Run your migrations in main.dart or a service provider:
void main() async {
await Magic.init(...);
// Run migrations
final migrations = await Migrator().run([
CreateUsersTable(),
CreatePostsTable(),
CreateCommentsTable(),
]);
if (migrations.isNotEmpty) {
Log.info('Ran ${migrations.length} migration(s)');
}
runApp(MagicApplication(...));
}
The Migrator keeps track of which migrations have already run, so calling run() multiple times is safe.
Creating Tables
Use Schema.create() to define a new table:
Schema.create('posts', (Blueprint table) {
table.id();
table.string('title');
table.text('content').nullable();
table.integer('user_id');
table.boolean('is_published').defaultValue(false);
table.timestamps();
});
Available Column Types
| Method | SQLite Type | Description |
|---|---|---|
id() |
INTEGER PRIMARY KEY | Auto-incrementing ID |
string(name) |
TEXT | String/varchar column |
text(name) |
TEXT | Long text content |
integer(name) |
INTEGER | Integer column |
bigInteger(name) |
INTEGER | Same as integer in SQLite |
boolean(name) |
INTEGER | 0 or 1 |
real(name) |
REAL | Floating point |
blob(name) |
BLOB | Binary data |
timestamps() |
TEXT × 2 | created_at & updated_at |
Column Modifiers
table.string('email').unique(); // Unique constraint
table.string('bio').nullable(); // Allow NULL
table.integer('status').defaultValue(0); // Default value
table.boolean('active').defaultValue(true);
Modifying Tables
Use Schema.table() to modify an existing table:
Schema.table('users', (Blueprint table) {
// Add new columns
table.string('avatar_url').nullable();
table.string('phone').nullable();
// Rename a column
table.renameColumn('name', 'full_name');
// Drop a column
table.dropColumn('legacy_field');
});
[!NOTE] Column dropping requires SQLite 3.35.0+ (2021). Column renaming requires SQLite 3.25.0+ (2018).
Modifying Column Types
SQLite does not support directly modifying column types. Use the add-copy-drop pattern:
@override
void up() {
// 1. Add new column
Schema.table('users', (table) {
table.string('name_new').nullable();
});
// 2. Copy data
DB.statement('UPDATE users SET name_new = name');
// 3. Drop old, rename new
Schema.table('users', (table) {
table.dropColumn('name');
table.renameColumn('name_new', 'name');
});
}
Dropping Tables
// Drop if exists (safe)
Schema.dropIfExists('temporary_data');
// Drop (throws if not exists)
Schema.drop('old_table');
// Rename table
Schema.rename('posts', 'articles');
Checking Schema
// Check if table exists
if (await Schema.hasTable('users')) {
// Table exists
}
// Check if column exists
if (await Schema.hasColumn('users', 'avatar_url')) {
// Column exists
}
// Get all column names
final columns = await Schema.getColumns('users');
for (final col in columns) {
print(col); // 'id', 'name', etc.
}
[!TIP] Always write both
up()anddown()methods to allow rolling back migrations during development.