GroupeZ Docs
Getting Started

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();
        });
    }
}

Defining down()

Implement down() to register the inverse operations of up(). For example, a migration that creates a table can drop it during rollback. The operations in down() should cover the changes you want to revert.

public class CreateUsersTable extends Migration {
    @Override
    public void up() {
        create("users", table -> {
            table.uuid("uuid").primary();
            table.string("name", 64);
        });
    }

    @Override
    public void down() {
        drop("users");
    }
}

The default method is empty. A migration whose down() declares no operation cannot be rolled back.

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:

import fr.maxlego08.sarah.SchemaBuilder;

public class AddEmailToUsers extends Migration {
    @Override
    public void up() {
        SchemaBuilder.alter(this, "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() {
        index("users", "name");
    }
}

Drop Table

Remove a table:

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

Rename Table

import fr.maxlego08.sarah.SchemaBuilder;

public class RenamePlayersTable extends Migration {
    @Override
    public void up() {
        SchemaBuilder.rename(this, "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)LONGTEXT / PostgreSQL: TEXTtable.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)BOOLEANtable.bool("active")
json(name)JSONtable.json("metadata")
blob(name)BLOB / PostgreSQL: BYTEAtable.blob("data")
timestamp(name)TIMESTAMPtable.timestamp("verified_at")
date(name)DATEtable.date("birth_date")

Types are adapted to the configured dialect. See the column type reference, including ENUM and PostgreSQL.

Column Modifiers

Primary Key

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

Auto Increment

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

On PostgreSQL, auto-incrementing keys use GENERATED BY DEFAULT AS IDENTITY.

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

On PostgreSQL, updated_at defaults to CURRENT_TIMESTAMP but does not change automatically on updates: assign it explicitly. These helpers use the connection dialect at execution time, without requiring MigrationManager.setDatabaseConfiguration() first.

Running Migrations

You can call execute() again in the same JVM without duplicating registered schemas. A migration is recorded after all its operations succeed. If an operation fails, the migration is not recorded; operations already completed are not automatically undone. If the migration table cannot be read, execution throws instead of treating every migration as new.

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);

Checking Migration Status

Read the names of applied migrations and the pending Migration objects. Pending migrations follow registration order. The tracking table is created if needed.

List<String> applied = MigrationManager.getAppliedMigrations(connection, logger);
List<Migration> pending = MigrationManager.getPendingMigrations(connection, logger);

Rolling Back Migrations

Roll back the last applied migration, or several migrations, and retrieve their names. Only migrations registered with the manager can be rolled back; they are processed in reverse registration order.

List<String> last = MigrationManager.rollback(connection, logger);
List<String> previous = MigrationManager.rollback(connection, logger, 2);

steps must be at least 1. Every selected migration must register operations in down(); otherwise an IllegalStateException is thrown before schema changes. After a successful rollback, its record is removed, so the next execute() can apply it again. If there is nothing to roll back, the result is an empty list.

A failed rollback throws DatabaseException. Migrations already rolled back stay rolled back; rolling back several migrations is not one global transaction.

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, JULogger.from(getLogger()));
            getLogger().info("Migrations completed successfully!");
        } catch (RuntimeException 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
getAppliedMigrations(connection, logger)Get applied migration names (List<String>)
getPendingMigrations(connection, logger)Get registered pending migrations (List<Migration>)
rollback(connection, logger)Roll back the last applied migration and return its name in a list
rollback(connection, logger, steps)Roll back the last applied migrations in reverse order
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:
import fr.maxlego08.sarah.SchemaBuilder;

public class AddAvatarToUsers extends Migration {
    @Override
    public void up() {
        SchemaBuilder.alter(this, "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() {
        SchemaBuilder.alter(this, "homes", table -> {
            table.string("icon", 64).defaultValue("GRASS_BLOCK");
        });
    }
}

Next Steps

On this page

GroupeZ MCP

Documentation in your AI

MCP lets your AI search GroupeZ documentation and read current examples to help you configure your plugins.

Read-only · No documentation API key needed

1. Copy the server URL

Transport: HTTP (Streamable HTTP). Authentication: none. Your tool uses its own model and subscription limits.

  1. Open Plugins in ChatGPT on the web, then + → Add custom MCP server.
  2. Name it ‘GroupeZ Documentation’, paste the URL above and choose ‘No authentication’. Create and install the plugin.
  3. In a conversation, type @ and select the plugin before asking. If needed, enable search, get_page and list_pages in its settings.
Official setup guide

3. Ask a question

Use GroupeZ MCP to help me create a zMenu inventory. Search relevant pages, read the examples and cite your sources.
Other clients and troubleshooting

For clients using mcpServers, adapt this example to their format:

{
  "mcpServers": {
    "groupez-docs": {
      "url": ""
    }
  }
}

Available tools are search, get_page and list_pages. Refresh the connector’s tools after updates. If your AI cannot access them, check that the connector is enabled in the conversation and your account allows custom MCP servers. Opening the URL in a browser is not an MCP test.