Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/main/java/com/javadiscord/javabot/data/MigrationUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.javadiscord.javabot.data;

import com.javadiscord.javabot.data.commands.MigrationsListSubcommand;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;

public class MigrationUtils {
public static Path getMigrationsDirectory() throws URISyntaxException, IOException {
var resource = MigrationsListSubcommand.class.getResource("/migrations/");
if (resource == null) throw new IOException("Missing resource /migrations/");
var uri = resource.toURI();
Path dirPath;
try {
dirPath = Paths.get(uri);
} catch (FileSystemNotFoundException e) {
var env = new HashMap<String, String>();
dirPath = FileSystems.newFileSystem(uri, env).getPath("/migrations/");
}
return dirPath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
public class DbAdminCommandHandler extends DelegatingCommandHandler {
public DbAdminCommandHandler() {
this.addSubcommand("export-schema", new ExportSchemaSubcommand());
this.addSubcommand("migrations-list", new MigrationsListSubcommand());
this.addSubcommand("migrate", new MigrateSubcommand());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.javadiscord.javabot.data.commands;

import com.javadiscord.javabot.Bot;
import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.commands.SlashCommandHandler;
import com.javadiscord.javabot.data.MigrationUtils;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.Objects;

/**
* This subcommand is responsible for executing SQL migrations on the bot's
* schema.
* <p>
* It uses the given name (adding .sql if it's not already there) to look
* for a matching file in the /migrations/ resource directory. Once it's
* found the file, it will split it up into a list of statements by the ';'
* character, and then proceed to execute each statement.
* </p>
*/
public class MigrateSubcommand implements SlashCommandHandler {
@Override
public ReplyAction handle(SlashCommandEvent event) {
String migrationName = Objects.requireNonNull(event.getOption("name")).getAsString();
if (!migrationName.endsWith(".sql")) {
migrationName = migrationName + ".sql";
}
try {
Path migrationsDir = MigrationUtils.getMigrationsDirectory();
Path migrationFile = migrationsDir.resolve(migrationName);
if (Files.notExists(migrationFile)) {
return Responses.warning(event, "The specified migration `" + migrationName + "` does not exist.");
}
String sql = Files.readString(migrationFile);
String[] statements = sql.split("\\s*;\\s*");
if (statements.length == 0) {
return Responses.warning(event, "The migration `" + migrationName + "` does not contain any statements. Please remove or edit it before running again.");
}
Bot.asyncPool.submit(() -> {
try (var con = Bot.dataSource.getConnection()) {
for (int i = 0; i < statements.length; i++) {
if (statements[i].isBlank()) {
event.getChannel().sendMessage("Skipping statement " + i + "; it is blank.").complete();
continue;
}
try (var stmt = con.createStatement()){
int rowsUpdated = stmt.executeUpdate(statements[i]);
event.getChannel().sendMessageFormat(
"Executed statement %d of %d:\n```\n%s\n```\nRows Updated: `%d`", i, statements.length, statements[i], rowsUpdated
).complete();
} catch (SQLException e) {
e.printStackTrace();
event.getChannel().sendMessage("Error while executing statement " + i + ": " + e.getMessage()).queue();
return;
}
}
} catch (SQLException e) {
event.getChannel().sendMessage("Could not obtain a connection to the database.").queue();
}
});
return Responses.info(event, "Migration Started", "Execution of the migration `" + migrationName + "` has been started. " + statements.length + " statements will be executed.");
} catch (IOException | URISyntaxException e) {
return Responses.error(event, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.javadiscord.javabot.data.commands;

import com.javadiscord.javabot.commands.Responses;
import com.javadiscord.javabot.commands.SlashCommandHandler;
import com.javadiscord.javabot.data.MigrationUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.stream.Collectors;

/**
* This subcommand shows a list of all available migrations, and a short preview
* of their source code.
*/
public class MigrationsListSubcommand implements SlashCommandHandler {
@Override
public ReplyAction handle(SlashCommandEvent event) {
try (var s = Files.list(MigrationUtils.getMigrationsDirectory())) {
EmbedBuilder embedBuilder = new EmbedBuilder()
.setTitle("List of Runnable Migrations");
var paths = s.filter(path -> path.getFileName().toString().endsWith(".sql")).collect(Collectors.toList());
if (paths.isEmpty()) {
embedBuilder.setDescription("There are no migrations to run. Please add them to the `/migrations/` resource directory.");
return event.replyEmbeds(embedBuilder.build());
}
paths.forEach(path -> {
StringBuilder sb = new StringBuilder(150);
sb.append("```sql\n");
try {
String sql = Files.readString(path);
sb.append(sql, 0, Math.min(sql.length(), 100));
if (sql.length() > 100) sb.append("...");
} catch (IOException e) {
e.printStackTrace();
sb.append("Error: Could not read SQL: ").append(e.getMessage());
}
sb.append("\n```");
embedBuilder.addField(path.getFileName().toString(), sb.toString(), false);
});
return event.replyEmbeds(embedBuilder.build());
} catch (IOException | URISyntaxException e) {
return Responses.error(event, e.getMessage());
}
}
}
11 changes: 10 additions & 1 deletion src/main/resources/commands.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -630,4 +630,13 @@
- name: include-data
description: Should data be included in the export?
required: true
type: BOOLEAN
type: BOOLEAN
- name: migrations-list
description: Show a list of all database migrations that can be run.
- name: migrate
description: Run a database migration.
options:
- name: name
description: The name of the migration to run.
required: true
type: STRING
4 changes: 4 additions & 0 deletions src/main/resources/migrations/test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Updates Andrew's account to have the specified number of credits.
UPDATE ECONOMY_ACCOUNT
SET BALANCE = 420
WHERE USER_ID = 235439851263098880;