Shipping SQL with a plugin
The model
A plugin never knows which database engine the server runs on. LonelyIce uses SQLite files by default and MySQL through a storage plugin; the same package must work on both. So:
- SQL files are written once, in the MySQL dialect AzerothCore's SQL has always used. The core translates each statement for the active backend when it applies the file.
- Plugin code reaches the databases only through the core's database interfaces. It never includes a driver.
- Backend-specific files and
overrides/folders are not allowed in plugins. The one exception is a database the plugin owns and updates itself (below).
The reference is Databases.
Declare the folders
"databases": {
"world": "data/sql/db-world",
"characters": "data/sql/db-characters"
}
| Key | Database |
|---|---|
auth |
accounts, realms (LoginDatabase) |
characters |
characters and everything they own (CharacterDatabase) |
world |
game content (WorldDatabase) |
Each value is a folder in the plugin, relative to plugin.json. The core's updater reads every *.sql file in it,
in subfolders too (so a module's base/ and updates/ subfolders work as they are). Put the folder under data/
or sql/: AddPlugin lays out only data, sql, conf, lua and client.
A real example is lonelyice.waystones:
data/sql/db-characters/2026_09_25_00_character_waystone.sql
data/sql/db-world/2026_09_25_01_waystones.sql
Name the files
- Files are applied in file-name order and tracked by file name in the database's
updatestable (stateMODULE). - A file name must be unique among all update folders of that database: the core's, every module's and every
plugin's. A duplicate stops the updater with
Duplicate filename. - Use
YYYY_MM_DD_NN_<plugin>_<what>.sql. The date orders your files; the plugin name keeps them apart from other plugins' files of the same day.
Write SQL that can run again
The updater stores a hash of each file. A file whose content changed is applied again. Write every file so that a second run does no harm:
-- characters: never drop player data
CREATE TABLE IF NOT EXISTS `character_waystone` (
`guid` INT UNSIGNED NOT NULL,
`waystone` INT UNSIGNED NOT NULL,
PRIMARY KEY (`guid`, `waystone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Attuned Kirin Tor waycrystals';
-- world: content you own can be replaced wholesale
DELETE FROM `creature_template` WHERE `entry` = 9100001;
INSERT INTO `creature_template` (`entry`, `name`, `subname`, `minlevel`, `maxlevel`, `faction`, `npcflag`, `ScriptName`) VALUES
(9100001, 'Waycrystal', 'Kirin Tor Translocation Network', 80, 80, 35, 1, 'npc_custom_waystone');
To change a table later, add a new file instead of editing an applied one.
When the SQL runs
In LonelyIce. The setup wizard applies the core's and all installed plugins' SQL once. After that the server
starts with database updates off. On every start it computes a stamp from the core database connections and, for
each loaded plugin, the id, version, database, relative path and SHA-256 of the contents of every .sql file in its
auth, characters and world folders. If the stamp differs from the one saved in plugins/.cache/sql.stamp, the
server runs the updater over the loaded plugins' folders only. A plugin installed, updated or edited later therefore
gets its tables on the next start.
While developing: any edit of a file's contents changes the stamp, so the next start runs the updater again and it
applies the changed file as described above. There is no need to delete sql.stamp. A database the plugin owns is not part of the stamp (see below).
In a plain worldserver of the fork. Plugin folders are added to the updater's list, so the SQL is applied at
start together with module SQL whenever Updates.EnableDatabases includes that database.
A plugin whose library does not load contributes no SQL.
Removing a plugin
Nothing undoes an update file. Tables and rows a plugin created stay in the database when it is disabled or
removed, and the updates table keeps its entries. Design for that: the server must run without the plugin even
though its tables exist.
SQL that must be undone on removal (for example rows that point at a spell id of the plugin) belongs in the
install and uninstall lists of a patch recipe, which run when the plugin is installed and removed. See
Client patches.
SQL the translation does not support
For SQLite each statement is translated. These are rejected there, so do not use them in plugin SQL:
| Not supported | Instead |
|---|---|
DELIMITER, stored procedures, CALL, PREPARE/EXECUTE, CREATE VIEW, triggers |
Plain statements, logic in C++ |
USE, LOAD DATA, HANDLER |
The updater picks the database from the folder |
System variables (@@...), INFORMATION_SCHEMA |
CREATE TABLE IF NOT EXISTS, DROP TABLE IF EXISTS |
:= assignments inside a statement |
SET @var = ...; as its own statement, then use @var |
UPDATE/DELETE ... LIMIT |
A WHERE on the key |
DELETE with a join, multi-table DELETE |
DELETE ... WHERE key IN (SELECT ...) |
UPDATE with an outer join, JOIN ... USING, updating a joined table |
UPDATE t SET ... WHERE key IN (SELECT ...) |
CREATE TABLE ... SELECT, CREATE TEMPORARY TABLE |
Create the table, then INSERT ... SELECT |
Generated columns, functional index parts, RENAME INDEX |
Plain columns and indexes |
ENGINE=, CHARSET, COMMENT, backquoted names, INSERT IGNORE, REPLACE and multi-row INSERT are translated.
Test every file on SQLite (the default storage) and, when you can, on MySQL.
Reaching the databases from code
Use the pools the core provides: LoginDatabase, CharacterDatabase, WorldDatabase (DatabaseEnv.h). Queries
are written in the same MySQL dialect and translated on the way:
#include "DatabaseEnv.h"
// asynchronous write
CharacterDatabase.Execute("INSERT IGNORE INTO `character_waystone` (`guid`, `waystone`) VALUES ({}, {})", guid, id);
// synchronous read
if (QueryResult result = CharacterDatabase.Query("SELECT `waystone` FROM `character_waystone` WHERE `guid` = {}", guid))
{
do
{
uint32 waystone = result->Fetch()[0].Get<uint32>();
// ...
} while (result->NextRow());
}
- Escape strings from players with
EscapeStringbefore putting them into a query. - The databases are not open while scripts are created (
AddScripts, constructors). Query them from hooks that run later, such asWorldScript::OnStartup. - The core's prepared statements belong to the core; a plugin cannot add statements to the core pools.
A database of your own
A plugin can own a whole database, as playerbots does. The core does not open it for you: your code derives a pool
from ModuleDatabasePool (with its own DatabaseConnection class and prepared statements), opens it in a
DatabaseScript hook OnModuleDatabasesLoading, and creates, populates and updates it with ModuleDBUpdater.
databases in the manifest may describe it as an object; the server and the launcher ignore that form:
"playerbots": { "config": "PlayerbotsDatabaseInfo", "base": "data/sql/playerbots/base", "updates": "data/sql/playerbots/updates" }
Your code runs this on every start, so the SQL stamp above does not cover it; playerbots does it under its own
switch Playerbots.Updates.EnableDatabases. With your plugin folder as the updater's source directory, a file of
that database that cannot be translated may get a replacement of the same name in
data/sql/overrides/<backend>/ (playerbots keeps overrides/sqlite/2025_04_26_00.sql). This is the only place a
plugin may use overrides; its auth, characters and world SQL must translate as written.
Mind that the LonelyIce launcher sets the connections of auth, characters, world and playerbots only, both
for SQLite files and for a database server. Another plugin-owned database gets no connection string from the
launcher; it has to come from your plugin's config. Read src/Script/Playerbots.cpp of mod-playerbots before
taking this route.