search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Migrations

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 up and down methods
  • Proper imports

[!NOTE] Migrations starting with create_ and ending with _table automatically use a special stub with Schema.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_table
  • 2024_01_15_120001_add_avatar_to_users
  • 2024_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() and down() methods to allow rolling back migrations during development.