fix: restore visibility arg on /core team create + auto-migrate leader_id

/core team create now takes [visibility] [leader] (both optional,
positional in this order). Variants:
- /core team create N T C                  → PRIVATE, leaderless
- /core team create N T C PUBLIC           → PUBLIC, leaderless
- /core team create N T C PRIVATE Alice    → PRIVATE, Alice as leader
- /core team create N T C PUBLIC Alice     → PUBLIC, Alice as leader

Auto-migration for pre-existing SQLite databases:
SqliteTeamRepository.ensureSchema() now checks pragma_table_info for the
leader_id column. If it still has the NOT NULL constraint from an older
schema, it recreates the table (in a transaction) with leader_id nullable
and copies the rows over. Required because CREATE TABLE IF NOT EXISTS
skips the alteration on existing tables, so production databases were
crashing with SQLITE_CONSTRAINT_NOTNULL when an admin tried to create a
leaderless team.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 15:28:48 +02:00
parent 5385b6e674
commit bcba8363c9
2 changed files with 66 additions and 16 deletions
@@ -8,6 +8,7 @@ import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamColor;
import fr.luc.crcore.team.TeamException;
import fr.luc.crcore.team.TeamService;
import fr.luc.crcore.team.TeamVisibility;
import org.bukkit.entity.Player;
import java.util.Objects;
@@ -15,21 +16,20 @@ import java.util.Optional;
import java.util.UUID;
/**
* {@code /core team create <name> <tag> <color> [leader]}
* {@code /core team create <name> <tag> <color> [visibility] [leader]}
*
* <p><b>Admin uniquement</b>. Crée une équipe en {@link
* fr.luc.crcore.team.TeamVisibility#PRIVATE} par défaut.
* <p><b>Admin uniquement</b>.
*
* <p>Les deux derniers arguments sont optionnels mais <b>positionnels</b> :
* pour passer un {@code leader}, il faut aussi taper la {@code visibility}
* d'abord (PUBLIC ou PRIVATE). Variantes valides :
*
* <p>Le chef est <b>optionnel</b> :
* <ul>
* <li>Sans argument {@code leader} → équipe leaderless. L'admin assignera
* plus tard via {@code /core team setleader}.</li>
* <li>Avec argument {@code leader} (nom d'un joueur connecté) → ce joueur
* devient chef et membre de l'équipe.</li>
* <li>{@code /core team create Wolves WOLF RED} → PRIVATE, leaderless</li>
* <li>{@code /core team create Wolves WOLF RED PUBLIC} → PUBLIC, leaderless</li>
* <li>{@code /core team create Wolves WOLF RED PRIVATE Alice} → Alice chef, PRIVATE</li>
* <li>{@code /core team create Wolves WOLF RED PUBLIC Alice} → Alice chef, PUBLIC</li>
* </ul>
*
* <p>La visibilité (PUBLIC/PRIVATE) se change ensuite via {@code /core team
* visibility} (action du chef).
*/
public class TeamCreateSubCommand extends SubCommand {
@@ -43,6 +43,7 @@ public class TeamCreateSubCommand extends SubCommand {
argument("name", ArgumentTypes.STRING);
argument("tag", ArgumentTypes.STRING);
argument("color", ArgumentTypes.enumOf(TeamColor.class));
optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
optionalArgument("leader", ArgumentTypes.ONLINE_PLAYER);
}
@@ -51,15 +52,18 @@ public class TeamCreateSubCommand extends SubCommand {
String name = ctx.get("name");
String tag = ctx.get("tag");
TeamColor color = ctx.get("color");
TeamVisibility visibility = ctx.<TeamVisibility>getOptional("visibility")
.orElse(TeamVisibility.PRIVATE);
Optional<Player> leaderOpt = ctx.getOptional("leader");
UUID leaderId = leaderOpt.map(Player::getUniqueId).orElse(null);
try {
Team team = service.createTeam(name, tag, color, leaderId);
String suffix = leaderOpt.isPresent()
? " (chef : " + leaderOpt.get().getName() + ")"
: " (sans chef)";
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée" + suffix + ".");
Team team = service.createTeam(name, tag, color, leaderId, visibility);
String chefPart = leaderOpt.isPresent()
? "chef : " + leaderOpt.get().getName()
: "sans chef";
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag()
+ "] créée (" + visibility + ", " + chefPart + ").");
} catch (TeamException ex) {
return CommandResult.failure(ex.getMessage());
}
@@ -69,6 +69,52 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
.column("score_name", ColumnType.TEXT).notNull()
.column("value", ColumnType.INTEGER).notNull()
.create();
// Migration : ancien schéma avait leader_id NOT NULL ; on le rend nullable
// pour permettre des équipes leaderless. SQLite ne supporte pas ALTER COLUMN,
// donc on recrée la table en copiant les données.
migrateLeaderIdToNullableIfNeeded();
}
/**
* Vérifie si la colonne {@code crcore_teams.leader_id} a une contrainte
* {@code NOT NULL} (ancien schéma) et la migre vers nullable si besoin.
* Idempotent — no-op si la colonne est déjà nullable ou si la table est
* neuve.
*/
private void migrateLeaderIdToNullableIfNeeded() {
boolean hasNotNull = db.queryOne(
"SELECT \"notnull\" FROM pragma_table_info('" + TABLE_TEAMS + "') WHERE name='leader_id'",
rs -> rs.getInt(1) == 1
).orElse(false);
if (!hasNotNull) return;
// Recréation via table temporaire — séquence standard SQLite pour
// changer une contrainte de colonne.
db.inTransaction(() -> {
db.execute("ALTER TABLE " + TABLE_TEAMS + " RENAME TO " + TABLE_TEAMS + "_old");
db.execute("CREATE TABLE " + TABLE_TEAMS + " ("
+ "\"id\" TEXT PRIMARY KEY, "
+ "\"name\" TEXT NOT NULL UNIQUE, "
+ "\"tag\" TEXT NOT NULL UNIQUE, "
+ "\"color\" TEXT NOT NULL, "
+ "\"leader_id\" TEXT, "
+ "\"visibility\" TEXT NOT NULL, "
+ "\"spawn_world\" TEXT, "
+ "\"spawn_x\" REAL, "
+ "\"spawn_y\" REAL, "
+ "\"spawn_z\" REAL, "
+ "\"spawn_yaw\" REAL, "
+ "\"spawn_pitch\" REAL"
+ ")");
db.execute("INSERT INTO " + TABLE_TEAMS + " "
+ "(id, name, tag, color, leader_id, visibility, "
+ " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) "
+ "SELECT id, name, tag, color, leader_id, visibility, "
+ " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch "
+ "FROM " + TABLE_TEAMS + "_old");
db.execute("DROP TABLE " + TABLE_TEAMS + "_old");
});
}
/** Recharge tous les Teams depuis la DB dans le cache mémoire hérité. */