Skip to main content

Database API

DatabaseManager (com.xreatlabs.xdiscordultimate.database.DatabaseManager) is your gateway to link state, support tickets, moderation logs, and player stats. It is backed by a HikariCP connection pool and an async-first execution model — never talk to it synchronously on the main thread.
DatabaseManager db = XDiscordUltimate.getInstance().getDatabaseManager();

Async-First Model

Every mutating or lookup operation runs through executeAsync, which off-threads the work onto the common pool and hands you back a CompletableFuture. The unit of work is the DatabaseOperation<T> functional interface:
@FunctionalInterface
public interface DatabaseOperation<T> {
    T execute() throws SQLException;
}

public <T> CompletableFuture<T> executeAsync(DatabaseOperation<T> operation);
This means every method below is non-blocking when called from the main thread:
db.getDiscordId(uuid)
    .thenAccept(id -> { /* id is null if not linked */ })
    .exceptionally(ex -> { ex.printStackTrace(); return null; });
Database calls in DatabaseManager run on the ForkJoinPool common pool. Always hop back to the Bukkit scheduler (Bukkit.getScheduler().runTask(...)) before touching the Bukkit API from a future’s callback.
public CompletableFuture<Boolean> linkAccount(UUID minecraftUuid,
                                              String discordId,
                                              String minecraftName,
                                              String discordName);
db.linkAccount(player.getUniqueId(), "123456789012345678",
               player.getName(), "DiscordUser")
  .thenAccept(success -> {
      if (success) getLogger().info("Linked " + player.getName());
  });
public CompletableFuture<Boolean> unlinkAccount(UUID minecraftUuid);
db.unlinkAccount(player.getUniqueId())
  .thenAccept(removed -> getLogger().info("Unlinked: " + removed));

Look up Discord from Minecraft (async)

public CompletableFuture<String> getDiscordId(UUID minecraftUuid); // null if not linked
db.getDiscordId(player.getUniqueId()).thenAccept(discordId -> {
    if (discordId == null) {
        // player is not linked
    }
});

Look up Minecraft from Discord

Async:
public CompletableFuture<UUID> getMinecraftUuid(String discordId); // null if not linked
Sync (use only from a worker thread):
public UUID getMinecraftUuidSync(String discordId);
public boolean isDiscordLinkedSync(String discordId);
// Inside your own async task:
UUID mcUuid = db.getMinecraftUuidSync("123456789012345678");
boolean linked = db.isDiscordLinkedSync("123456789012345678");

Full account snapshot

getLinkedAccountInfoSync returns an immutable LinkedAccountInfo (or null):
public LinkedAccountInfo getLinkedAccountInfoSync(String discordId);
DatabaseManager.LinkedAccountInfo info =
    db.getLinkedAccountInfoSync("123456789012345678");

if (info != null) {
    String discordId   = info.getDiscordId();
    String discordName = info.getDiscordName();
    String mcName      = info.getMinecraftName();
    UUID   mcUuid      = info.getMinecraftUuid();
    Timestamp linkedAt = info.getLinkedAt();
}
LinkedAccountInfo getters: getDiscordId(), getDiscordName(), getMinecraftName(), getMinecraftUuid(), getLinkedAt().

Tickets, Moderation, and Stats

These helpers cover the rest of the schema:
MethodReturnsNotes
createTicket(UUID player, String subject)CompletableFuture<Integer>New ticket ID
updateTicketChannel(int ticketId, String channelId)CompletableFuture<Boolean>Attach Discord channel
closeTicket(int ticketId)CompletableFuture<Boolean>Mark CLOSED
addTicketMessage(int ticketId, UUID sender, String senderType, String message)CompletableFuture<Boolean>Append a message
getOpenTicketsCount(UUID player)CompletableFuture<Integer>Open ticket count
logModerationAction(String type, UUID target, UUID moderator, String reason, Timestamp expires)CompletableFuture<Void>Audit row
createReport(UUID reporter, UUID target, String reason)CompletableFuture<Integer>New report ID
hasRecentReport(UUID reporter, int cooldownMinutes)CompletableFuture<Boolean>Cooldown check
updatePlayerStats(UUID player, String statType, int increment)CompletableFuture<Void>Increment a stat column
updateDiscordName(String discordId, String newName)CompletableFuture<Boolean>Refresh stored display name

Raw SQL Escape Hatch

For anything the typed methods don’t cover, borrow a connection straight from the pool. Always close it in a try-with-resources and always parameterize user input.
public Connection getConnection() throws SQLException;
try (Connection conn = db.getConnection();
     PreparedStatement stmt = conn.prepareStatement(
         "SELECT verified_at FROM verified_users WHERE discord_id = ?")) {

    stmt.setString(1, discordId);
    try (ResultSet rs = stmt.executeQuery()) {
        if (rs.next()) {
            return rs.getTimestamp("verified_at");
        }
    }
} catch (SQLException e) {
    getLogger().warning("Query failed: " + e.getMessage());
}
Never build SQL with string concatenation. Discord IDs and usernames come from untrusted sources — concatenate and you ship a SQL injection. Use ? placeholders and the setXxx setters, every time.

Verification Codes

The verification flow stores short-lived codes (sync API — safe because they are only touched from Discord events, which run off the main thread):
db.storeVerificationCode(discordId, "ABC123", "Steve", 10); // 10-minute TTL

DatabaseManager.VerificationCode vc = db.getVerificationCode("ABC123");
if (vc != null && !vc.isExpired()) {
    String username = vc.getUsername();
    db.removeVerificationCode(vc.getCode());
}

db.cleanExpiredVerificationCodes(); // periodic sweep
VerificationCode getters: getDiscordId(), getCode(), getUsername(), getCreatedAt(), getExpiresAt(), plus isExpired().

Schema

XDiscordUltimate creates and migrates these tables on startup:
TableKey columns
verified_usersminecraft_uuid (PK), discord_id (UNIQUE), verified_at, minecraft_name, discord_name
verification_codesdiscord_id (PK), code (UNIQUE), username, created_at, expires_at
ticketsid, minecraft_uuid, discord_channel_id, status, created_at, closed_at, subject
ticket_messagesid, ticket_id, sender_uuid, sender_type, sent_at
moderation_logsid, action_type, moderator_uuid, reason, timestamp, expires_at, active
player_statsminecraft_uuid (PK), joins, messages_sent, deaths, playtime_minutes, last_seen, first_join
activity_rewardsid, discord_id, reward_type, reward_data, claimed, created_at, claimed_at
schema_versionsversion_key (PK), version_value, applied_at

Connection Pool

All connections come from a HikariCP pool with these settings:
SettingValue
maximumPoolSize10
minimumIdle2
idleTimeout600000 ms
connectionTimeout30000 ms
leakDetectionThreshold60000 ms

Next Steps

Event Bus

React to PlayerVerifiedEvent instead of polling the database.

API Overview

Back to the API introduction and core services.