Skip to main content

Migrations

Migrations allow you to version control your database schema. Sarah tracks which migrations have been executed and ensures each migration runs only once.

How Migrations Work​

  1. You create migration classes that define schema changes
  2. Sarah maintains a migrations table to track executed migrations
  3. When you call MigrationManager.execute(), only new migrations run
  4. Migrations are executed in the order they were registered

Creating a Migration​

Extend the Migration class and implement the up() method:

import fr.maxlego08.sarah.database.Migration;

public class CreateUsersTable extends Migration {

@Override
public void up() {
create("users", table -> {
table.uuid("uuid").primary();
table.string("name", 64);
table.text("location").nullable();
table.bigInt("play_time").defaultValue(0);
table.timestamps();
});
}
}

Migration Operations​

Create Table​

public class CreatePlayersTable extends Migration {
@Override
public void up() {
create("players", table -> {
table.uuid("uuid").primary();
table.string("name", 32);
table.decimal("balance", 10, 2).defaultValue(0);
table.bool("is_banned").defaultValue(false);
table.timestamps();
});
}
}

Create Table from Class Template​

You can automatically create a table based on a record or class:

// Define your DTO
public record PlayerDTO(
UUID uuid,
String name,
long playTime
) {}

// Migration
public class CreatePlayersFromTemplate extends Migration {
@Override
public void up() {
create("players", PlayerDTO.class);
}
}

Alter Table​

Modify an existing table:

public class AddEmailToUsers extends Migration {
@Override
public void up() {
alter("users", table -> {
table.string("email", 255).nullable();
});
}
}

Create Index​

Create an index for better query performance:

public class CreateUsersNameIndex extends Migration {
@Override
public void up() {
createIndex("users", "name");
}
}

Drop Table​

Remove a table:

public class DropOldLogsTable extends Migration {
@Override
public void up() {
drop("old_logs");
}
}

Rename Table​

public class RenamePlayersTable extends Migration {
@Override
public void up() {
rename("players", "users");
}
}

Column Types​

Sarah supports various column types:

MethodSQL TypeExample
uuid(name)VARCHAR(36)table.uuid("player_id")
string(name, length)VARCHAR(length)table.string("name", 64)
text(name)TEXTtable.text("description")
longText(name)LONGTEXTtable.longText("content")
integer(name)INTtable.integer("count")
bigInt(name)BIGINTtable.bigInt("balance")
decimal(name)DECIMALtable.decimal("price")
decimal(name, length, decimals)DECIMAL(length, decimals)table.decimal("price", 10, 2)
bool(name)TINYINT(1)table.bool("active")
json(name)JSONtable.json("metadata")
blob(name)BLOBtable.blob("data")
timestamp(name)TIMESTAMPtable.timestamp("verified_at")
date(name)DATEtable.date("birth_date")

Column Modifiers​

Primary Key​

table.uuid("uuid").primary();

Auto Increment​

table.autoIncrement("id");        // INT
table.autoIncrementBigInt("id"); // BIGINT

Nullable​

table.string("nickname", 32).nullable();

Default Value​

table.bigInt("balance").defaultValue(0);
table.bool("active").defaultValue(true);
table.string("status", 16).defaultValue("pending");

Default Current Timestamp​

table.timestamp("created_at").defaultCurrentTimestamp();

Unique​

table.string("email", 255).unique();

Foreign Key​

table.uuid("user_id").foreignKey("users");
// or with custom column and cascade
table.uuid("user_id").foreignKey("users", "uuid", true);

Timestamps Helper​

Add created_at and updated_at columns:

create("posts", table -> {
table.autoIncrementBigInt("id");
table.string("title", 255);
table.timestamps(); // Adds created_at and updated_at
});

Or individually:

table.createdAt();  // Just created_at
table.updatedAt(); // Just updated_at

Running Migrations​

Basic Execution​

// Set custom table name (optional, defaults to "migrations")
MigrationManager.setMigrationTableName("my_plugin_migrations");

// Register migrations in order
MigrationManager.registerMigration(new CreateUsersTable());
MigrationManager.registerMigration(new CreatePostsTable());
MigrationManager.registerMigration(new AddEmailToUsers());

// Execute all pending migrations
MigrationManager.execute(databaseConnection, logger);

Complete Example​

public class MyPlugin extends JavaPlugin {

@Override
public void onEnable() {
// Setup database connection
DatabaseConnection connection = setupConnection();

// Configure migration table name
MigrationManager.setMigrationTableName("myplugin_migrations");

// Register all migrations
registerMigrations();

// Run migrations
try {
MigrationManager.execute(connection, getLogger());
getLogger().info("Migrations completed successfully!");
} catch (SQLException e) {
getLogger().severe("Migration failed: " + e.getMessage());
e.printStackTrace();
}
}

private void registerMigrations() {
MigrationManager.registerMigration(new CreatePlayersTable());
MigrationManager.registerMigration(new CreateHomesTable());
MigrationManager.registerMigration(new CreateWarpsTable());
MigrationManager.registerMigration(new AddPlayerEmailColumn());
}
}

Migration Manager Methods​

MethodDescription
setMigrationTableName(name)Set custom migrations table name
getMigrationTableName()Get current migrations table name
registerMigration(migration)Register a migration to be executed
getMigrations()Get list of registered migrations
execute(connection, logger)Execute all pending migrations
setDatabaseConfiguration(config)Set the database configuration
getDatabaseConfiguration()Get the database configuration

Best Practices​

1. Name Migrations Descriptively​

// Good
public class CreateUsersTable extends Migration { }
public class AddEmailColumnToUsers extends Migration { }
public class CreateUserPostsIndex extends Migration { }

// Bad
public class Migration1 extends Migration { }
public class UpdateTable extends Migration { }

2. Keep Migrations Small​

Each migration should do one thing:

// Good - separate migrations
public class CreateUsersTable extends Migration { ... }
public class CreatePostsTable extends Migration { ... }

// Bad - too much in one migration
public class CreateAllTables extends Migration { ... }

3. Never Modify Existing Migrations​

Once a migration has been executed in production, create a new migration for changes:

// Don't modify CreateUsersTable after it's deployed
// Instead, create a new migration:
public class AddAvatarToUsers extends Migration {
@Override
public void up() {
alter("users", table -> {
table.string("avatar_url", 255).nullable();
});
}
}

4. Use Timestamps​

Always include timestamps for auditing:

create("important_data", table -> {
table.autoIncrementBigInt("id");
table.string("data", 255);
table.timestamps(); // Always include this
});

Real-World Example​

Here's a complete migration setup for a homes plugin:

// Migration 1: Create players table
public class CreatePlayersTable extends Migration {
@Override
public void up() {
create("homes_players", table -> {
table.uuid("uuid").primary();
table.string("name", 16);
table.integer("max_homes").defaultValue(3);
table.timestamps();
});
}
}

// Migration 2: Create homes table
public class CreateHomesTable extends Migration {
@Override
public void up() {
create("homes", table -> {
table.autoIncrementBigInt("id");
table.uuid("player_uuid").foreignKey("homes_players", "uuid", true);
table.string("name", 32);
table.string("world", 64);
table.decimal("x", 10, 2);
table.decimal("y", 10, 2);
table.decimal("z", 10, 2);
table.decimal("yaw", 5, 2);
table.decimal("pitch", 5, 2);
table.timestamps();
});
}
}

// Migration 3: Add icon column
public class AddIconToHomes extends Migration {
@Override
public void up() {
alter("homes", table -> {
table.string("icon", 64).defaultValue("GRASS_BLOCK");
});
}
}

Next Steps​

Copyright © 2026 GroupeZ|Build #loading...|-