9 Database
Axionize WS2 edited this page 2026-05-03 23:33:08 -04:00

Database

Grim stores violation history, session metadata, player identity, persistent player settings, and future blob-style records in the datastore. The current public configuration is split into one routing file and one file per backend:

  • plugins/GrimAC/database.yml decides whether the datastore is enabled and which backend each category uses.
  • plugins/GrimAC/databases/<backend-id>.yml contains the connection settings for that backend.

By default, violation, session, player-identity, and setting route to SQLite; blob routes to none.

database:
  enabled: true
  routing:
    violation: sqlite
    session: sqlite
    player-identity: sqlite
    setting: sqlite
    blob: none

The supported backend ids are:

  • memory - no setup, process-local, lost on restart. Use only when you intentionally want ephemeral storage.
  • sqlite - embedded file, no separate server. Default and recommended for single-server setups.
  • mysql - MySQL / MariaDB, good when several Grim instances share history.
  • postgres - PostgreSQL, same multi-server use case as MySQL.
  • mongo - MongoDB document store, useful if you already operate Mongo.
  • redis - Redis KV store; only use for history when Redis persistence is configured and the durability tradeoff is acceptable.

Tip

If you only need history for one server and do not already run a database, stay on SQLite. It needs no driver install on Bukkit/Spigot/Paper, and the default file is plugins/GrimAC/data/history.v1.db.


Drivers - What's Already On The Classpath

Most Bukkit-family servers ship the SQLite and MySQL JDBC drivers as part of the server itself, so on those platforms Grim can use them directly with no extra install. PostgreSQL, MongoDB, and Redis drivers are never bundled by any server flavour, so those need a holder plugin/mod or another classpath source. Fabric and NeoForge ship no DB drivers at all, so every persistent backend needs a holder mod there.

Each holder mod is a single universal jar that works on Spigot/Paper, Fabric, Forge, and NeoForge from one file. Drop it next to Grim; install nothing else.

Backend Bukkit / Spigot / Paper Fabric / NeoForge
SQLite Built in on supported Bukkit-family servers. Optional holder: minecraft-sqlite-jdbc. Install minecraft-sqlite-jdbc.
MySQL / MariaDB Built in on supported Bukkit-family servers. Optional holder: minecraft-mysql-jdbc. Install minecraft-mysql-jdbc.
PostgreSQL Install minecraft-postgresql-jdbc. Install minecraft-postgresql-jdbc.
MongoDB Install minecraft-mongodb-driver. Install minecraft-mongodb-driver.
Redis Install minecraft-jedis. Install minecraft-jedis.

Important

On Bukkit/Spigot/Paper, the bundled sqlite-jdbc and mysql-connector-j live on the server's parent classloader. Plugin classloaders delegate to the parent first, so for the default JDBC path (DriverManager.getConnection) the bundled driver normally wins regardless of what is in plugins/. Grim works fine on Bukkit-family servers out of the box using the bundled drivers.

If you want a newer SQLite engine than what your server bundles, install the holder mod and Grim will detect it via its public API and route through the holder's child-first classloader when the holder is strictly newer. See Using a newer SQLite engine on a Bukkit-family server.

Tip

Holder plugins are completely Grim-agnostic. They just ship standard JDBC / native drivers as Minecraft plugins/mods. Other plugins using the same drivers can benefit from the same install.

SQLite Engine Versions Across Server Lines

For reference if you are wondering what SQLite version your server actually ships:

Server line Bundled SQLite engine Grim's SQLite backend
CraftBukkit 1.8 - 1.10 3.7.2 works (legacy dialect)
CraftBukkit 1.11 3.16.1 works (legacy dialect)
CraftBukkit 1.12 3.21.0.1 works (legacy dialect)
CraftBukkit 1.13.2+ 3.25.2 or newer works (modern dialect)
Paper 1.21.4+ 3.47.0.0 or newer works (modern dialect)
Paper master / 26.x 3.49.1.0+ works (modern dialect)

Grim's SQLite backend probes sqlite_version() at startup and picks one of two writer dialects: a single-statement INSERT ... ON CONFLICT DO UPDATE upsert on engine 3.24+, or a two-statement INSERT OR IGNORE + UPDATE fallback in a transaction on older engines. Both produce the same row state. Schema is portable across supported MC versions; see Database - History for the file-portability story.

Using A Newer SQLite Engine On A Bukkit-Family Server

If you specifically want an engine version newer than what the server bundles for the modern UPSERT path on a legacy server, for RETURNING / STRICT support, or just to track the latest Xerial release, install minecraft-sqlite-jdbc alongside Grim.

The holder mod ships a public API class (dev.axionize.sqlite_jdbc.MinecraftSqliteJdbc) that wraps a child-first URLClassLoader pointing at its own jar, parented to the platform classloader so org.sqlite.* comes from the holder rather than the server's parent chain. A Driver instance returned from this loader has its own class identity, distinct from the bundled copy.

Grim probes for this API at SQLite-backend init via reflection. If the holder is present and its engine version is strictly newer than the bundled driver's, every JDBC connection Grim opens routes through the holder's classloader. You will see log lines like:

[grim-datastore] minecraft-sqlite-jdbc holder is newer than bundled
  (holder=3.53.0, bundled=3.21.0); routing JDBC through holder's child-first classloader
[grim-datastore] SQLite engine 3.53.0 - using modern dialect

This means a Paper 1.8 server with bundled SQLite 3.7.2 plus the holder mod gets the same modern UPSERT writers as a recent Paper server. Same schema, no Grim code changes.

If you want the same trick from your own plugin, softdepend on sqlite-jdbc (the holder's Bukkit name) and call MinecraftSqliteJdbc.connect(url) directly. The returned Connection is a standard java.sql.Connection; keep casts to java.sql.* interfaces, not connector-internal types. See the holder's Modrinth page for the full API and caveats. The MySQL holder uses the same pattern.


Picking And Configuring Backends

Edit plugins/GrimAC/database.yml to route categories. Each distinct backend id in routing: must have a matching file under plugins/GrimAC/databases/<backend-id>.yml.

Example: shared history on MySQL, local settings on SQLite:

database:
  routing:
    violation: mysql
    session: mysql
    player-identity: mysql
    setting: sqlite
    blob: none

SQLite

plugins/GrimAC/databases/sqlite.yml:

sqlite:
  path: "data/history.v1.db"
  journal-mode: WAL
  synchronous-mode: NORMAL
  busy-timeout-ms: 5000
  cache-pages: 10000
  batch-flush-cap: 256

No driver install needed on Bukkit/Spigot/Paper; the driver is already bundled. On Fabric or NeoForge, install minecraft-sqlite-jdbc.

SQLite is single-server storage. Networked setups should route history to a backend that talks to a shared database.

MySQL / MariaDB

plugins/GrimAC/databases/mysql.yml:

mysql:
  host: "localhost"
  port: 3306
  database: "grim"
  user: "root"
  password: ""
  extra-jdbc-params: ""
  batch-flush-cap: 256
  tables:
    meta: grim_meta
    checks: grim_checks
    players: grim_players
    sessions: grim_sessions
    violations: grim_violations
    settings: grim_settings

The connection URL Grim builds includes useSSL=false, allowPublicKeyRetrieval=true, and rewriteBatchedStatements=true automatically. Append anything else, such as TLS overrides or timeouts, via extra-jdbc-params.

MariaDB works through the same MySQL backend.

MariaDB support

MariaDB is supported through the mysql backend id and the same databases/mysql.yml file. There is no separate mariadb backend id and no separate MariaDB JDBC driver requirement; Grim connects through Connector/J and then probes SELECT VERSION() during backend initialization.

If the server reports MariaDB 10.6 or newer, Grim selects its MariaDB dialect. The MariaDB dialect uses MariaDB's supported ON DUPLICATE KEY UPDATE ... VALUES(column) upsert form, while genuine MySQL uses the MySQL 8.0.19+ aliased-row upsert form. Both flavors share the same v8 table shape, including the stored generated current_name_lower column used for indexed case-insensitive player-name lookup.

MariaDB older than 10.6 is rejected at startup with a clear datastore error. The hard floor is intentionally conservative: generated columns need MariaDB 10.2.1+, and 10.6 is the oldest MariaDB LTS line Grim treats as supported.

Startup logs identify the chosen flavor:

[grim-datastore] MariaDB engine 10.11.16-MariaDB-... - using MariaDB dialect

PostgreSQL

plugins/GrimAC/databases/postgres.yml:

postgres:
  host: "localhost"
  port: 5432
  database: "grim"
  user: "postgres"
  password: ""
  extra-jdbc-params: ""
  batch-flush-cap: 256
  tables:
    meta: grim_meta
    checks: grim_checks
    players: grim_players
    sessions: grim_sessions
    violations: grim_violations
    settings: grim_settings

Useful additions in extra-jdbc-params: sslmode=require, reWriteBatchedInserts=true, ApplicationName=Grim.

MongoDB

plugins/GrimAC/databases/mongo.yml:

mongo:
  connection-string: "mongodb://localhost:27017"
  database: "grim"
  batch-flush-cap: 256
  tables:
    meta: grim_meta
    checks: grim_checks
    players: grim_players
    sessions: grim_sessions
    violations: grim_violations
    settings: grim_settings

Use the connection string for replica set members, auth source, read preference, and other Mongo options.

Redis (Jedis)

plugins/GrimAC/databases/redis.yml:

redis:
  host: "localhost"
  port: 6379
  database: 0
  user: ""
  password: ""
  key-prefix: ""
  timeout-ms: 2000
  batch-flush-cap: 256
  warn-on-history: true
  tables:
    meta: grim_meta
    checks: grim_checks
    players: grim_players
    sessions: grim_sessions
    violations: grim_violations
    settings: grim_settings

Warning

Redis is appropriate for low-latency writes but trades durability for throughput. If your Redis instance flushes or you hit maxmemory eviction, history can be lost. Use one of the relational or document backends if durability matters.


Datastore Settings

database.yml also controls session reconstruction, the async write path, migration, name resolution, and /grim history paging:

database:
  session:
    gap-ms: 600000
    scope-per-server: true
    heartbeat-interval-ms: 30000

  write-path:
    queue-capacity: 16384
    batch-size: 256
    flush-interval-ms: 1000
    warn-rate-ms: 10000
    shutdown-drain-timeout-ms: 5000
    wait-strategy: BLOCKING

  retention:
    session:
      enabled: true
      max-age-days: 90
    violation:
      enabled: true
      max-age-days: 365
    player-identity:
      enabled: false
    setting:
      enabled: false
    blob:
      enabled: true
      max-age-days: 30

  migration:
    skip: false
    max-duration-ms: 0

  name-resolution:
    chain: [local-cache, offline-mode-uuid]

  history:
    entries-per-page: 15
    group-interval-ms: 30000

  server-name: "Prison"

write-path.queue-capacity must be a positive power of two. Valid wait strategies are BLOCKING, TIMEOUT_BLOCKING, SLEEPING, YIELDING, and BUSY_SPIN.

server-name is a display-only tag stamped on new session records and shown in /grim history. Changing it does not rewrite old sessions.


Verifying It Worked

After restart, check paper.log or your platform equivalent for a line like:

[grim-datastore] SQLite engine 3.49.1 - using modern dialect

For non-SQLite backends, look for the backend connect/init log. If a driver is missing or the backend cannot initialize, Grim logs the backend error and then logs that datastore initialization failed.


Failure Handling

If database.enabled is false, datastore services are skipped. Live anti-cheat checks still run, but violation history, /grim history, persisted player toggles, and datastore-backed identity lookup are unavailable.

Current public behavior: if a configured backend's driver is missing or the database is unreachable during startup, datastore initialization fails and the datastore is disabled for that run. Grim does not silently fall back to memory; route a category to memory explicitly if you want ephemeral storage.

/grim reload tears down the current datastore and rebuilds backends plus routing from the current database.yml and databases/*.yml. Writes during the brief teardown/rebuild window can be dropped.

Planned but not shipped: ordered backend chains for a category, such as session: [mysql, sqlite, memory], with boot-time selection of the first healthy backend. Hot failover after a backend drops mid-session is not current behavior.


Subsystems

Different parts of Grim use the database differently. See the dedicated pages:

  • History - /grim history, violation records, sessions
  • Settings - persisted player toggles and internal key/value settings

More information in the readme.