Queries¶
Every query in mysql_samp is non-blocking by default: the statement runs on a worker thread and the result reaches your script on a later tick, so the server does not freeze waiting for the database.
Two things do block, and both are deliberate and named:
mysql_connectwaits for the handshake, which is why it belongs inOnGameModeInitand not in a player callback.- A call that is given
MYSQL_SYNCin place of a callback name blocks on purpose, for the cases where waiting is worth more than the tick — schema at start-up, a migration, a one-off command.
Nothing blocks unless you wrote one of those two, and the second one says so at the call site.
The callback is optional. Non-blocking does not mean "you have to write a callback for everything" — every native on this page takes callback with a default of "", and passing nothing means fire-and-forget:
// One line, no callback, no forward, no public. The server keeps running.
mysql_query(g_mysql, "UPDATE players SET last_login = NOW() WHERE id = 1");
That covers most of what a script does with a database: UPDATE, INSERT, DELETE. You only need a callback when you want to read the result back, because a value that arrives later has nowhere else to arrive. A failure still reaches OnQueryError either way, so a discarded result is not a silent one.
The two exceptions are mysql_hash_password and mysql_verify_password: their callback is required, since the hash or the match result exists only there. Called with an empty callback they log a warning and return false.
mysql_query — FIFO-ordered¶
native bool:mysql_query(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...);
| Parameter | Type | Description |
|---|---|---|
connId |
int | Connection id returned by mysql_connect |
query |
string | Raw SQL statement (already escaped — use mysql_format to build it safely) |
callback |
string | Pawn public to invoke when the result is ready. Empty ("") means fire-and-forget |
format |
string | One char per variadic parameter (d/i = int, f = float, s = string) |
... |
variadic | Values forwarded to the callback in the order they appear |
Returns: true if the query was queued, false if connId is unknown or the submission itself failed — the server refusing a new worker thread, which only happens under a burst of thousands in a single tick. A refused submission fires OnQueryError with errorid 0, so a FIFO chain never waits on a callback that is not coming.
Ordering guarantee — read this one carefully. What is ordered is the delivery of callbacks, in submission order, even when the underlying queries finish out of order. A slow query holds back the dispatch of later callbacks until it completes.
Execution is not ordered. Each query gets its own worker thread and its own pool connection the moment you submit it, so they run at the same time on the server. Two statements where the second depends on the first will race:
// BROKEN: these run concurrently. The INSERT usually fails with
// "Table 'x' doesn't exist" because the CREATE has not finished yet.
mysql_query(g_mysql, "CREATE TABLE x (id INT)");
mysql_query(g_mysql, "INSERT INTO x (id) VALUES (1)");
When one statement depends on another, pick the tool that actually serialises them:
| What you need | Use |
|---|---|
Schema setup, migrations, any .sql script |
mysql_query_file — statements run in order, on one connection |
| Writes that depend on each other | mysql_transaction_* — one connection, in order, all-or-nothing |
| A read that depends on a write | The callback of the write |
mysql_query(g_mysql, "SELECT * FROM players WHERE level > 5", "OnHighLevelPlayers");
forward OnHighLevelPlayers();
public OnHighLevelPlayers()
{
new rows = cache_get_row_count();
printf("high-level players: %d", rows);
}
Passing parameters to the callback¶
The format string declares the types; the variadics carry the actual values. The callback sees the parameters in left-to-right order.
mysql_format(g_mysql, query, sizeof(query),
"SELECT * FROM players WHERE id = %d", target_id);
mysql_query(g_mysql, query, "OnPlayerLoaded", "d", playerid);
forward OnPlayerLoaded(playerid);
public OnPlayerLoaded(playerid)
{
if (cache_get_row_count() > 0)
{
new name[MAX_PLAYER_NAME];
cache_get_value_name(0, "name", name);
printf("player %d: %s", playerid, name);
}
}
Callback format characters:
| Char | Pawn type | Notes |
|---|---|---|
d, i |
int | Both aliases work |
f |
float | |
s |
string | Pushed as an AMX string allocation |
Any other character logs a warning at submission time and is skipped.
Fire and forget¶
Omit the callback to discard the result:
A failure still fires OnQueryError.
MYSQL_SYNC — making one call block, on purpose¶
There is no synchronous native, and there will not be one: a native named
mysql_query_sync is copied around a gamemode until something blocks inside
OnPlayerUpdate. What exists instead is a value you write where the callback
name goes, which turns that one call — and only that one — into a blocking
call:
mysql_query(g_mysql, "SELECT COUNT(*) AS n FROM accounts", MYSQL_SYNC);
new total = cache_get_value_name_int(0, "n");
The call returns when the database answers, and the result is the current cache on the very next line. No forward, no public, and the value is available in the same scope that asked for it.
When it is worth it¶
Only when nobody is online to feel it, and the alternative is worse:
- Schema and migrations at start-up. The strongest case, because each statement depends on the previous one having finished, and threaded calls run concurrently:
public OnGameModeInit()
{
g_mysql = mysql_connect(...);
mysql_query_file(g_mysql, "schema.sql", MYSQL_SYNC);
// From here on the tables exist. No callback chain, no race.
return 1;
}
- A one-off console or admin command where reading the answer inline keeps the code honest and the freeze is a few milliseconds.
When it is wrong¶
Never in anything that runs per player or per tick. OnPlayerConnect,
OnPlayerText, a timer: the whole server stops for as long as the database
takes. On a local database that is a few milliseconds; on a remote one, or one
under load, it is the difference between a server and a slideshow. A query that
takes 2 s costs every player 2 s — measured, not estimated.
The rules¶
- It is refused inside a callback. There, the current cache already belongs
to the callback, and a blocking result would either shadow it or be
unreadable — both surprises. The call is not run,
falsecomes back and the reason is logged. Run it before the callback, or use a threaded call. - The result lives for the current frame. It is freed on the next server
tick, the same way a threaded result is freed when its callback returns.
Read it on the lines right after the call — which is the whole point — and
use
cache_save()if it has to last longer. Nothing to free by hand: the plugin does not ask you to remember acache_delete, and this does not become the exception. - One at a time within the frame. A second blocking call replaces the
first, and
cache_unset_active()drops it early. - Errors behave the same as threaded ones.
falsecomes back,mysql_errnois set andOnQueryErrorfires, withMYSQL_SYNCas the callback name so the handler can tell where it came from. - Supported on
mysql_query,mysql_pqueryandmysql_query_file. Onmysql_pqueryit behaves exactly likemysql_query, since a blocking call has nothing to run in parallel with. Everywhere else — prepared statements, transactions, the ORM, password hashing — it is refused with a message naming what to use instead, so it can never be mistaken for a callback that silently never fires.
mysql_pquery — parallel, no ordering¶
native bool:mysql_pquery(connId, const query[], const callback[] = "", const format[] = "", {Float,_}:...);
Same signature as mysql_query, same callback semantics, but no ordering guarantee. Callbacks fire as soon as each query finishes; the dispatcher places parallel results behind ordered ones in the same tick.
Use mysql_pquery when the gamemode does not care which callback runs first.
mysql_pquery(g_mysql, "UPDATE stats SET kills = kills + 1 WHERE id = 1");
mysql_pquery(g_mysql, "INSERT INTO logs (action) VALUES ('kill')");
mysql_pquery(g_mysql, "SELECT * FROM rewards WHERE id = 1", "OnRewards");
Choosing between mysql_query and mysql_pquery¶
| Concern | mysql_query |
mysql_pquery |
|---|---|---|
| Threading | One worker thread per query | One worker thread per query |
| Callback order | FIFO (submission order) | First done, first dispatched |
| Execution order | Not ordered — concurrent | Not ordered — concurrent |
| Typical use | Reads whose callbacks must arrive in order | UPDATE/INSERT and independent reads |
| Reordering cost | Yes — results buffer until the next sequence is available | None |
Both natives create one OS thread per query — the cost of a thread spawn is roughly the cost of one TCP round-trip, well below the cost of a real MySQL query.
mysql_tick — backwards compatibility¶
Drains the threaded-query queue manually. You do not need to call this. Since rust-samp v3.0.0, the unified on_tick hook is driven by ProcessTick on SA-MP and by an ITimersComponent 5 ms timer on Open Multiplayer native mode; the plugin processes pending results automatically. mysql_tick is kept only so old gamemodes that called it explicitly still compile and behave.
mysql_format¶
printf-style query builder with automatic SQL escaping on %s / %e. Returns the byte length of the rendered string (after truncation, if any). The connId parameter is currently unused but kept for API compatibility — pass the connection you intend to run the query on.
| Specifier | Type | Behavior |
|---|---|---|
%d, %i |
int | Decimal integer |
%f |
float | 4 decimal places ({:.4}) |
%s, %e |
string | SQL-escaped (safe to drop into a quoted string literal) |
%r |
string | Raw, not escaped — only for trusted constants |
%% |
literal | Single % |
Any other %x is left as-is in the output and a single warning is logged for the whole call.
Truncation¶
If the rendered string is longer than max_len - 1 (the -1 reserves a slot for the AMX NUL terminator), the plugin truncates at the nearest UTF-8 char boundary, logs a warning and still returns the length actually written. Old behavior aborted the native; the new behavior is deterministic and survives oversized inputs gracefully.
Example¶
new query[256];
new name[24] = "O'Brien";
mysql_format(g_mysql, query, sizeof(query),
"SELECT * FROM players WHERE name = '%s'", name);
// Result: SELECT * FROM players WHERE name = 'O\'Brien'
mysql_query(g_mysql, query, "OnPlayerFound");
Combining types:
new query[256];
mysql_format(g_mysql, query, sizeof(query),
"INSERT INTO scores (player_id, score, name) VALUES (%d, %f, '%s')",
playerid, 99.5, "Player1");
Rule of thumb. Use
%sfor every string that originated outside your code. Use%ronly for compile-time constants such as table names. Never pass user input through%r.
mysql_escape_string¶
Returns true on success, false only if writing into dest fails.
connId selects the escaping rules, which depend on the server's sql_mode. Pass the real connection. The parameter is optional purely so older code keeps compiling; the default assumes standard MySQL rules, which are wrong — and unsafe — on a server running NO_BACKSLASH_ESCAPES. See Security.
Under the default sql_mode, the characters escaped are \0, \n, \r, \\, ', ", \x1a (Ctrl-Z). Every other byte (including \t, control bytes, and multi-byte UTF-8) passes through unchanged.
new escaped[128];
new input[] = "It's a \"test\"";
mysql_escape_string(input, escaped);
// escaped = "It\'s a \"test\""
Never call mysql_escape_string on a string and then pass the result through %s — %s escapes again and you end up with \\' instead of \'.
Prepared statements¶
The safe alternative to escaping. Values are bound server-side over the binary protocol, so they never enter the SQL text — there is nothing to escape and nothing to get wrong. Use these for anything carrying player input.
native mysql_stmt_new(connId, const query[]);
native bool:mysql_stmt_bind_int(stmtId, value);
native bool:mysql_stmt_bind_float(stmtId, Float:value);
native bool:mysql_stmt_bind_str(stmtId, const value[]);
native bool:mysql_stmt_bind_null(stmtId);
native bool:mysql_stmt_reset(stmtId);
native bool:mysql_stmt_execute(stmtId, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:mysql_stmt_pexecute(stmtId, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:mysql_stmt_close(stmtId);
new stmt = mysql_stmt_new(g_mysql, "SELECT id FROM players WHERE name = ? AND score > ?");
mysql_stmt_bind_str(stmt, playerName);
mysql_stmt_bind_int(stmt, 100);
mysql_stmt_execute(stmt, "OnPlayersFound", "d", playerid);
mysql_stmt_close(stmt);
mysql_stmt_execute is non-blocking and FIFO-ordered, exactly like mysql_query, and the result reaches the callback through the same cache stack. mysql_stmt_pexecute is the parallel counterpart, matching mysql_pquery: no ordering guarantee, dispatched as soon as it completes. Choose between them on the same grounds as mysql_query vs mysql_pquery.
Notes:
- Bind order matters — values are positional, matching the
?placeholders left to right. - Counts must match. If the number of bound values differs from the number of placeholders,
mysql_stmt_executereturnsfalseand logs both numbers instead of sending a broken query. mysql_stmt_resetkeeps the statement, drops the values — useful for bulk inserts.- A
?stands for a value, never an identifier.SELECT * FROM ?is not valid SQL. Build identifiers from a whitelist you control. - Values stay out of the logs. Only the template with its placeholders reaches
logs/mysql.logandcache_get_query_string(). - Placeholders are counted outside of quoted strings and comments, so a literal
?inside'...'or/* ... */is not mistaken for one.
Transactions¶
Runs a group of statements atomically: START TRANSACTION, every step, COMMIT. Any failing step rolls the whole batch back and fires OnQueryError.
native mysql_transaction_new(connId);
native bool:mysql_transaction_add(txId, const query[]);
native bool:mysql_transaction_add_stmt(txId, stmtId);
native bool:mysql_transaction_execute(txId, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:mysql_transaction_destroy(txId);
new tx = mysql_transaction_new(g_mysql);
mysql_transaction_add(tx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
mysql_transaction_add(tx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");
mysql_transaction_execute(tx, "OnTransferDone", "d", playerid);
Notes:
- There is no interactive begin/commit. Holding a pooled connection between server ticks would leak it whenever a gamemode never reached the commit — an early
return, a runtime error, a disconnect. Collecting the steps first avoids that entirely. mysql_transaction_executeconsumes the handle. The ID is invalid afterwards, so a batch cannot run twice by accident.- The callback receives the cache of the last step, which is where
cache_affected_rowsandcache_insert_idare usually wanted. - Use
mysql_transaction_add_stmtfor player input — it copies a prepared statement together with its bound values. - No auto-reconnect retry. Replaying a transaction after a mid-flight connection loss could re-apply steps the server already committed, so a dropped connection aborts and reports instead.
- Build a batch and change your mind? Call
mysql_transaction_destroy.
Running a .sql file¶
native bool:mysql_query_file(connId, const path[], const callback[] = "", const format[] = "", {Float,_}:...);
Reads a file and runs its statements in order on one connection, non-blocking like every other query here. Useful for schema setup and migrations.
The path is resolved against the server's working directory — the folder holding samp03svr or omp-server — not against scriptfiles/. A path that does not resolve reaches OnQueryError with the name it tried, and errorid 0, since nothing was sent to the server.
The script is split on ; outside string literals and comments, so a semicolon inside '...', "...", a backtick identifier, -- …, # … or /* … */ does not break a statement in two. Comment-only fragments and a trailing semicolon are dropped rather than sent as empty statements.
Notes:
- It is not a transaction. These files are usually schema work, and DDL commits implicitly in MySQL, so wrapping them would imply an atomicity the server does not provide. If a statement fails, execution stops there and everything before it stays applied.
- The error names the position —
statement 7 of 23: …— which matters in a file with dozens of statements. - The callback receives the cache of the last statement, and
cache_get_result_count()reports how many result sets the script produced. - The path is a file your gamemode opens. Treat it as such: do not build it from player input.
- The plugin logs how many statements it is about to run, at
INFO.
Inspecting the queue¶
Returns in_flight + pending_ordered.len() — queries currently spawned on a worker thread plus queries whose results arrived but whose callbacks are blocked behind an earlier sequence.
Useful for graceful shutdown loops: keep ticking until mysql_unprocessed_queries() == 0.
Result limits¶
| Limit | Value | Behavior on overflow |
|---|---|---|
| Rows per result | 100 000 | Drained but discarded; a warning is logged once per oversized query |
| Saved caches | 1 024 | cache_save() returns 0 and logs a warning |
| ORM string buffer | 4 096 chars | orm_addvar_string returns false |