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
6 changes: 2 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Fixed

- The `Dev Release` workflow now retries publishing the `dev` prerelease before giving up. The release and its tag have to be deleted and recreated for the tag to move to the new commit, and a transient API failure inside that window previously left the repository with no `dev` release at all until the workflow was re-run by hand. Each attempt now starts from a clean slate, and an exhausted retry fails loudly.

### Added

- A `Dev Release` workflow, which republishes a rolling `dev` prerelease of `main` on every non-documentation push. This is what Dan's Plugin Manager's experimental channel installs from: `/dpm get alternateaccountfinder --experimental` reads `releases/tags/dev`, so without it there is nothing for that command to download. The prerelease is unreleased, unreviewed code and is marked as such.

### Fixed

- A misspelled or empty `database.dialect` in `config.yml` is now reported as a startup message naming the key, quoting the offending value and giving the two dialects the plugin ships drivers for, after which the plugin disables itself. Previously the value went straight to jOOQ, which answered with `No enum constant org.jooq.SQLDialect.mariadb` — a message that names neither `config.yml` nor an acceptable value — and did so only after the connection pool had already been opened. The value is also matched case-insensitively now, so `h2` and `mariadb` are accepted alongside `H2` and `MARIADB` (see [#101](https://github.com/Dans-Plugins/AlternateAccountFinder/issues/101)).
- The `Dev Release` workflow now retries publishing the `dev` prerelease before giving up. The release and its tag have to be deleted and recreated for the tag to move to the new commit, and a transient API failure inside that window previously left the repository with no `dev` release at all until the workflow was re-run by hand. Each attempt now starts from a clean slate, and an exhausted retry fails loudly.
- The database connection pool is now closed when the plugin is disabled. It previously stayed open for the lifetime of the server process, so every `/reload` — and every disable performed by a plugin manager such as Dan's Plugin Manager — left the old pool's connections and threads running while the next startup built a second pool beside them (see [#97](https://github.com/Dans-Plugins/AlternateAccountFinder/issues/97)).

## [3.0.0-SNAPSHOT-8-8-2026] – 2026-08-08
Expand Down
2 changes: 1 addition & 1 deletion CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ database:

**Type:** string
**Default:** `H2`
**Description:** The SQL dialect that jOOQ uses when generating queries. Set to `H2` for the embedded database or `MARIADB` for MariaDB/MySQL.
**Description:** The SQL dialect that jOOQ uses when generating queries. Set to `H2` for the embedded database or `MARIADB` for MariaDB/MySQL. Case does not matter, so `h2` and `mariadb` work too. If the key is set to an empty value, or to something that is not a dialect the plugin can use, startup stops with a message naming the key and the accepted values, and the plugin disables itself rather than running against the wrong dialect. Removing the key entirely is not an error — a key absent from your file falls back to the default above.

**Example:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;

public final class AlternateAccountFinder extends JavaPlugin implements Listener {
Expand All @@ -43,6 +44,17 @@ public final class AlternateAccountFinder extends JavaPlugin implements Listener
public void onEnable() {
saveDefaultConfig();

// Configuration is validated before anything is opened, so a rejected dialect leaves no
// connection pool behind.
SQLDialect dialect;
try {
dialect = parseDialect(getConfig().getString("database.dialect"));
} catch (IllegalArgumentException exception) {
getLogger().severe(exception.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
}

// Ensure database drivers are loaded
try {
Class.forName("org.h2.Driver");
Expand Down Expand Up @@ -82,7 +94,6 @@ public void onEnable() {
// jOOQ
System.setProperty("org.jooq.no-logo", "true");
System.setProperty("org.jooq.no-tips", "true");
SQLDialect dialect = SQLDialect.valueOf(getConfig().getString("database.dialect"));
Settings jooqSettings = new Settings().withRenderSchema(false);
DSLContext dsl = DSL.using(
dataSource,
Expand Down Expand Up @@ -153,6 +164,37 @@ static void closeDataSource(DataSource dataSource, Logger logger) {
}
}

/**
* Resolves the {@code database.dialect} config value to the jOOQ dialect it names.
*
* <p>The value is matched case-insensitively, so {@code h2} and {@code mariadb} name the same
* dialects as {@code H2} and {@code MARIADB}. Any other dialect jOOQ recognises is still
* accepted: an operator pointing the plugin at MySQL through the MariaDB driver depends on
* that, and only the two dialects this plugin ships drivers for are named in the message.
*
* @throws IllegalArgumentException if the value is blank or is not a dialect jOOQ knows.
* {@code SQLDialect.valueOf} throws for the same cases, but
* with a message that mentions neither {@code config.yml} nor
* the offending key. A {@code null} value is treated as blank;
* a key an operator has deleted resolves to the bundled
* {@code config.yml} default rather than to {@code null}, so
* that branch only guards against the bundled default itself
* going missing.
*/
static SQLDialect parseDialect(String configuredDialect) {
if (configuredDialect == null || configuredDialect.isBlank()) {
throw new IllegalArgumentException("database.dialect is not set in config.yml. "
+ "Set it to H2 for the embedded database, or MARIADB for MariaDB/MySQL.");
}
try {
return SQLDialect.valueOf(configuredDialect.strip().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("database.dialect in config.yml is set to \""
+ configuredDialect + "\", which is not a dialect this plugin can use. "
+ "Set it to H2 for the embedded database, or MARIADB for MariaDB/MySQL.", exception);
}
}

public LoginService getLoginService() {
return loginService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.jooq.SQLDialect;
import org.junit.jupiter.api.Test;

import javax.sql.DataSource;
Expand All @@ -19,15 +20,16 @@

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Tests for the connection-pool shutdown helper (issue #97).
* Tests for the connection-pool shutdown helper (issue #97) and the dialect parser (issue #101).
*
* <p>{@link AlternateAccountFinder} extends {@code JavaPlugin} and cannot be constructed outside a
* running server, so {@code onDisable} itself is not covered here — only the static helper it
* delegates to. That the helper is actually wired into {@code onDisable} stays a manual check on a
* live server.
* running server, so neither {@code onDisable} nor {@code onEnable} is covered here — only the
* static helpers they delegate to. That those helpers are actually wired in, and that a rejected
* dialect disables the plugin rather than throwing, stay manual checks on a live server.
*/
class AlternateAccountFinderTest {

Expand Down Expand Up @@ -97,6 +99,66 @@ void warnsRatherThanThrowsWhenTheCloseFails() {
assertEquals("pool is stuck", logged.get(0).getThrown().getMessage());
}

@Test
void parsesTheDialectsTheDefaultConfigDocuments() {
assertEquals(SQLDialect.H2, AlternateAccountFinder.parseDialect("H2"));
assertEquals(SQLDialect.MARIADB, AlternateAccountFinder.parseDialect("MARIADB"));
}

@Test
void parsesADialectWhateverCaseItIsWrittenIn() {
// SQLDialect's constants are uppercase, so the spellings an operator reaches for first
// used to be rejected outright.
assertEquals(SQLDialect.MARIADB, AlternateAccountFinder.parseDialect("mariadb"));
assertEquals(SQLDialect.MARIADB, AlternateAccountFinder.parseDialect("MariaDB"));
assertEquals(SQLDialect.H2, AlternateAccountFinder.parseDialect("h2"));
}

@Test
void parsesADialectSurroundedByWhitespace() {
assertEquals(SQLDialect.H2, AlternateAccountFinder.parseDialect(" H2 "));
}

@Test
void stillAcceptsADialectThisPluginShipsNoDriverFor() {
// Narrowing the accepted set to H2 and MariaDB would break an operator running MySQL
// through the MariaDB driver, so anything jOOQ recognises is let through.
assertEquals(SQLDialect.MYSQL, AlternateAccountFinder.parseDialect("MYSQL"));
}

@Test
void rejectsAMissingDialectWithAMessageNamingTheConfigKey() {
// A key left out of the operator's config.yml resolves to the jar's bundled H2 default
// rather than to null, so this branch is defensive: it covers the case where the bundled
// config is what has lost the key. Enum.valueOf answered null with "Name is null".
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> AlternateAccountFinder.parseDialect(null));

assertTrue(exception.getMessage().contains("database.dialect"), exception.getMessage());
assertTrue(exception.getMessage().contains("config.yml"), exception.getMessage());
assertTrue(exception.getMessage().contains("H2"), exception.getMessage());
assertTrue(exception.getMessage().contains("MARIADB"), exception.getMessage());
}

@Test
void rejectsABlankDialectTheSameWayAsAMissingOne() {
assertEquals(
assertThrows(IllegalArgumentException.class, () -> AlternateAccountFinder.parseDialect(null))
.getMessage(),
assertThrows(IllegalArgumentException.class, () -> AlternateAccountFinder.parseDialect(" "))
.getMessage());
}

@Test
void rejectsAnUnknownDialectWithAMessageQuotingTheOffendingValue() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> AlternateAccountFinder.parseDialect("postgres-ish"));

assertTrue(exception.getMessage().contains("database.dialect"), exception.getMessage());
assertTrue(exception.getMessage().contains("\"postgres-ish\""), exception.getMessage());
assertTrue(exception.getMessage().contains("MARIADB"), exception.getMessage());
}

/**
* A logger that collects what it is given instead of printing it, so a test can assert on the
* warning without the failure path also cluttering the build output.
Expand Down
Loading