Initialize oxmysql before calling MySQL
The Lua MySQL wrapper comes from @oxmysql/lib/MySQL.lua. Declaring dependency 'oxmysql' establishes resource dependency order but does not import that wrapper into your script. Load it first, keep database code server-side and bind values as parameters. oxmysql targets MariaDB/MySQL, not PostgreSQL.
This standalone exercise writes only to a disposable docs_db_probe table. It demonstrates SQL commit/rollback, not a framework inventory, money transfer or authorization system. Use an isolated development database and back up existing data before applying any schema change.
1. Create the table and resource
Follow database connection setup first. Import this schema.sql into the development database selected by your connection string:
CREATE TABLE docs_db_probe (
id INT NOT NULL PRIMARY KEY,
message VARCHAR(100) NOT NULL
) ENGINE=InnoDB;Create resources/[local]/docs_db_probe/fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
dependency 'oxmysql'
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server.lua'
}The table uses InnoDB so its writes participate in transactions. The resource has no client or shared script. Do not put connection strings into files downloaded to players.
2. Add a console-only transaction probe
Save this as server.lua in that resource:
local busy = false
RegisterCommand('docsdb', function(playerSource, args)
if playerSource ~= 0 then return end
local mode = args[1]
if mode ~= 'reset' and mode ~= 'commit' and mode ~= 'rollback' and mode ~= 'read' then
print('[docsdb] Use: docsdb reset|commit|rollback|read')
return
end
if busy then
print('[docsdb] A probe is already running.')
return
end
busy = true
local ok = pcall(function()
if mode == 'reset' then
MySQL.update.await('DELETE FROM docs_db_probe')
elseif mode == 'commit' or mode == 'rollback' then
local secondId = mode == 'rollback' and 1 or 2
local success = MySQL.transaction.await({
{ query = 'INSERT INTO docs_db_probe (id, message) VALUES (?, ?)',
values = { 1, 'first statement' } },
{ query = 'INSERT INTO docs_db_probe (id, message) VALUES (?, ?)',
values = { secondId, 'second statement' } }
})
print(('[docsdb] committed=%s'):format(tostring(success)))
end
local count = MySQL.scalar.await('SELECT COUNT(*) FROM docs_db_probe')
print(('[docsdb] rows=%s'):format(tostring(count)))
end)
busy = false
if not ok then
print('[docsdb] Probe failed; inspect private database/resource logs, then run docsdb read.')
end
end, true)The explicit console check prevents a connected player’s command from doing database writes. Parameters bind values, not SQL identifiers: table/column names remain fixed. The local busy flag prevents this resource’s console probes from interleaving while an awaited call yields; it is not a cross-resource database lock.
3. Start in order and verify results
In the active server.cfg, configure the private connection before starting oxmysql, then start the consumer:
set mysql_connection_string "mysql://fivem_user:[email protected]:3306/fivem?charset=utf8mb4"
ensure oxmysql
ensure docs_db_probeReplace the connection template with your development account. In the server console, use refresh if the new resource has not been discovered. Check oxmysql startup and the first error before invoking commands.
| Commands, in the server console | Expected table state |
|---|---|
docsdb reset, then docsdb read | Zero rows; reset deletes only this disposable table’s rows |
docsdb commit, then docsdb read | committed=true; two rows |
docsdb reset, then docsdb rollback, then docsdb read | Duplicate primary key in the second INSERT; transaction fails and zero rows remain |
docsdb commit twice without reset | The repeated INSERT fails; previously committed rows remain |
| A connected player calls the command | No database operation |
The transaction API returns a boolean. The wrapper/driver can also report errors; the pcall prevents a rejected await from leaving this probe permanently busy. The duplicate-key error in the rollback test is intentional. Confirm the final table state with docsdb read and, independently, a SQL client. Stop the demo resource when finished. Do not run reset against another resource’s tables.
SQL success is not business success
An UPDATE that matches zero rows can still be a successful SQL statement. A transaction may commit even when an application condition, such as sufficient balance or a current session, was not satisfied. Do not combine a conditional debit and unconditional credit and assume a true transaction boolean makes the transfer safe.
Real purchases require server-owned prices, authenticated identity, concurrency-safe invariant checks, appropriate locking/atomic statements and idempotent request handling. Follow the exact database/driver transaction capabilities and test concurrent requests against your schema. A client-supplied amount or a client-side busy flag is not an authorization boundary. See secure events and async session lifetime.
Migrating legacy database APIs
MySQL.Async.execute is a mysql-async-style interface, not proof that a standalone ghmattimysql installation supplies the same global. The ghmattimysql API documents resource exports such as exports.ghmattimysql:execute. oxmysql has compatibility aliases, but installation, imports, parameter semantics and return values still need review for the exact consumer version.
Migrate one adapter deliberately: inventory imports/exports, back up schema/data, test callback and await error paths, and verify transaction behavior before switching production. Do not run multiple adapters against assumed interchangeable APIs just to suppress an initialization error.
For MySQL nil, connection refusal, schema mismatch and startup order, use database setup, resource lifecycle and common errors.