Skip to content

Validate database.dialect before opening the connection pool - #102

Merged
dmccoystephenson merged 2 commits into
mainfrom
fix/database-dialect-validation
Aug 15, 2026
Merged

Validate database.dialect before opening the connection pool#102
dmccoystephenson merged 2 commits into
mainfrom
fix/database-dialect-validation

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

  • database.dialect is now resolved through a package-private AlternateAccountFinder#parseDialect
    helper rather than handed straight to SQLDialect.valueOf. A blank or unrecognised value is
    rejected with a message that names the key, quotes the offending value, and gives the two dialects
    the plugin ships drivers for; the plugin then disables itself instead of letting the exception
    escape onEnable as a Bukkit stack trace.
  • The value is matched case-insensitively and stripped of surrounding whitespace, so h2,
    mariadb and MariaDB are accepted alongside the uppercase spellings CONFIG.md documents. Any
    other dialect jOOQ recognises is still accepted, since narrowing the set to H2 and MARIADB
    would break an operator running MySQL through the MariaDB driver.
  • The check runs before the HikariCP pool is built, so a configuration that is going to be rejected
    no longer opens a pool first.
  • The duplicated ### Fixed heading under [Unreleased] in CHANGELOG.md was reconciled into one
    section, with ### Added moved above it. CONTRIBUTING.md and CLAUDE.md both require each
    heading to appear at most once, in Keep a Changelog order; CHANGELOG.md has two ### Changed sections under the same release #92 fixed the same shape before and it
    had reappeared.
  • CONFIG.md now records the case-insensitive matching and what happens when the key is unusable.

Correction carried from triage

An earlier draft of #101 claimed that a key absent from the operator's config.yml reaches
SQLDialect.valueOf(null). It does not: JavaPlugin#reloadConfig installs the jar's bundled
config.yml as the configuration's defaults, so an absent key resolves to the shipped H2. The
issue body has been corrected, the null branch is kept as a defensive guard only, and the
CHANGELOG.md entry describes the reachable failures. The test covering the null branch says the
same thing in a comment rather than implying an operator can reach it.

Test plan

  • AlternateAccountFinderTest gains seven cases over parseDialect: the two documented
    dialects, mixed and lower case, surrounding whitespace, a dialect the plugin ships no driver
    for, a null value, a blank value, and an unknown value whose message must quote what was
    configured.
  • ./gradlew clean build could not be run locally — the sandbox this was written in has only a
    JDK 21, which Gradle 8.1.1 refuses (Unsupported class file major version 65), and no JDK 17
    is reachable there. The Build check on this pull request's head commit is the anchor for
    this change; it must be green before merge.
  • Manual, on a live server, for what no unit test here reaches: set database.dialect to
    mariadb and confirm startup succeeds; set it to postgres and confirm the startup log names
    the key and the accepted values and that the plugin is disabled rather than throwing; confirm
    no connection pool is opened on that rejected path.

Coverage gap

The Build workflow exercises the JVM build against H2 only. It cannot run the Bukkit path this
change touches — the disablePlugin call and the wiring of parseDialect into onEnable — nor a
MariaDB connection. Green CI therefore verifies the helper and the existing suite, not the startup
behaviour; the manual step above is what covers that. No migration, schema or encryption path is
touched by this change.

Deferred this cycle

The rest of the open backlog was left alone, with reasons:

Closes #100
Closes #101

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).

dmccoystephenson and others added 2 commits August 15, 2026 12:45
SQLDialect.valueOf received the raw config value, so a misspelling or a
lowercase spelling failed startup with "No enum constant
org.jooq.SQLDialect.mariadb" and nothing naming config.yml, after the
HikariCP pool had already been built.

Parse the value through a package-private helper instead: match it
case-insensitively, reject a blank or unknown value with a message that
names the key and the two dialects the plugin ships drivers for, and run
the check before any resource is opened so a rejected configuration
disables the plugin rather than leaving a pool behind.

Also reconcile the duplicated ### Fixed heading under [Unreleased],
which CONTRIBUTING.md and CLAUDE.md both forbid.

Closes #100
Closes #101

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A key absent from the operator's config.yml resolves to the bundled
default rather than to null, since JavaPlugin installs the jar's
config.yml as the configuration's defaults. Saying that a missing key
stops startup was wrong; only an empty or unusable value does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric, scored against the diff and the Build check on head cde5420:

  • CI: PASSbuild passed in 54s on cde5420 (run 31885561569), the exact head of this branch. Local execution of ./gradlew clean build was not possible: the sandbox this was written in carries only a JDK 21, which Gradle 8.1.1 rejects with Unsupported class file major version 65, and no JDK 17 is reachable from it. The CI check on the head commit is therefore the anchor for everything below.
  • Scope: PASS — four files, and each is needed. AlternateAccountFinder.java and its test carry A missing or misspelled database.dialect fails startup with a bare NullPointerException #101; CHANGELOG.md carries both CHANGELOG.md [Unreleased] has two ### Fixed sections, in the wrong order #100 and the entry A missing or misspelled database.dialect fails startup with a bare NullPointerException #101 requires; CONFIG.md is the sources-of-truth row that owns the database.dialect key. No formatting, rename or comment churn rode along.
  • Tests-new: PASSparseDialect is the only new method, and seven cases exercise it: both documented dialects, mixed and lower case, surrounding whitespace, a dialect no bundled driver backs, null, blank, and an unknown value whose message must quote what was configured.
  • Tests-fix: UNVERIFIED by construction — the stash-and-run this rubric asks for could not be performed, for the JDK reason above, and it would not be informative here in any case: the method under test did not exist before this change, so reverting the source hunk makes the new tests fail to compile rather than fail an assertion. What the tests do pin is the message content — key name, quoted value, and both accepted dialects — which is the part of the fix an operator actually sees, and that would fail if the messages regressed.
  • Docs: PASS after a fix — this item failed on the first pass and was corrected in cde5420. CONFIG.md had been written to say that a missing database.dialect key stops startup. It does not: JavaPlugin#reloadConfig installs the jar's bundled config.yml as the configuration's defaults, so a key an operator deletes resolves to the shipped H2. CONFIG.md:35 — the description now says an empty or unusable value is what stops startup, and states explicitly that removing the key falls back to the default. The same imprecision was removed from the helper's @throws text, which now records that the null branch only guards the bundled default itself going missing. The remaining rows of the sources-of-truth table are unaffected: no command, permission, config key, migration or README-level behaviour changed.
  • Issue resolution: PASSCHANGELOG.md [Unreleased] has two ### Fixed sections, in the wrong order #100: [Unreleased] now holds one ### Added followed by one ### Fixed, which is the Keep a Changelog order CONTRIBUTING.md and CLAUDE.md require, and no entry text was altered in the move. A missing or misspelled database.dialect fails startup with a bare NullPointerException #101: the parse is validated, case-insensitive, and moved ahead of the pool.
  • Sibling structure / sibling renames: PASS — no new file and no renamed identifier; parseDialect is placed beside closeDataSource, the existing package-private static helper on the same class, and is documented in the same style.
  • Privacy: PASS — nothing in the diff reads, stores, logs or suggests an address. The only new output is a startup log line quoting a configured dialect string.
  • plugin.yml permissions / commands: PASS (not applicable)plugin.yml is untouched; no permission is checked and no subcommand is routed by this change.
  • @OverRide coverage: PASS — no method overriding a Bukkit or superclass method was added. parseDialect is a static helper.
  • MariaDB compatibility: PASS (not applicable) — no SQL and no Flyway migration in the diff.
  • Encryption determinism: PASS (not applicable)IpEncryption and every lookup path over the encrypted column are untouched.
  • Migration safety: PASS (not applicable)migrateExistingIpAddresses and the ip-migration-v2.complete marker are untouched.

Judgment calls left for a human reviewer, folded in here rather than as inline notes:

  • src/main/java/com/dansplugins/detectionsystem/AlternateAccountFinder.java:53 — disablePlugin(this) is called from inside onEnable. This is the conventional Bukkit way for a plugin to refuse a bad configuration, and it is safe here (onDisable closes a dataSource that is still null at this point, so the shutdown path is a no-op), but the server still fires PluginEnableEvent afterwards for a plugin that is by then disabled. That sequence cannot be exercised by any test in this repository and is what the manual step in the description covers.
  • src/main/java/com/dansplugins/detectionsystem/AlternateAccountFinder.java:190 — the rejection message names only H2 and MARIADB while the parser accepts any dialect jOOQ recognises. That is deliberate: narrowing the accepted set would break an operator reaching MySQL through the MariaDB driver, while naming every jOOQ dialect in an error message would bury the two that ship with drivers. The reasoning is recorded in the helper's javadoc so a later reader does not "fix" the apparent mismatch.

Green CI here means the JVM build against H2 passed. It does not exercise the Bukkit startup path this change touches, nor a MariaDB connection; the manual checks in the description are what cover those.

This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).

@dmccoystephenson
dmccoystephenson merged commit 8835300 into main Aug 15, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the fix/database-dialect-validation branch August 15, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant