ORM¶
The ORM maps Pawn variables to MySQL columns. With it, SELECT / INSERT / UPDATE / DELETE / save are one-liners — the plugin generates the SQL from the bound variables.
Concept¶
- Create an ORM instance attached to a table and a connection.
- Bind Pawn variables to columns (
orm_addvar_*). - Declare the primary key column (
orm_setkey). - Run CRUD operations — every call reads the current values from the bound variables, generates the SQL, and runs the query through the standard threaded pipeline.
The five threaded CRUD natives (orm_select, orm_update, orm_insert, orm_delete, orm_save) accept an optional callback with the same format string convention as mysql_query (d/i, f, s).
ORM instances are owned per-AMX. When the AMX that created the instance unloads, the plugin destroys every ORM bound to it automatically — Plugin::on_amx_unload calls OrmManager::destroy_by_amx. You do not need to clean up on script unload.
Lifecycle¶
orm_create¶
Creates an ORM bound to table on connId. Returns the ORM id (>= 1) or 0 if connId does not exist.
orm_destroy¶
Returns true if the ORM existed and was removed, false otherwise.
new g_orm;
public OnGameModeInit()
{
g_orm = orm_create("players", g_mysql);
// ... bind variables, set key ...
}
public OnGameModeExit()
{
orm_destroy(g_orm);
}
Variable bindings¶
native bool:orm_addvar_int(orm_id, &var, const column_name[]);
native bool:orm_addvar_float(orm_id, &Float:var, const column_name[]);
native bool:orm_addvar_string(orm_id, var[], var_max_len, const column_name[]);
native bool:orm_delvar(orm_id, const column_name[]);
native bool:orm_clear_vars(orm_id);
native bool:orm_setkey(orm_id, const column_name[]);
orm_addvar_int/orm_addvar_floattake Pawn variables by reference — the ORM stores the AMX address, not the value, so changes from Pawn are picked up on the next query build.orm_addvar_stringrequires1 <= var_max_len <= 4096. Values outside that range are rejected (false). The 4096 cap protects the AMX heap against oversized writes whenorm_apply_cachecopies a column into the Pawn buffer.orm_delvar(column_name)removes the binding whose column name matches exactly (case-sensitive).orm_clear_vars(orm_id)drops every binding.orm_setkey(column_name)declares which bound column is the primary key. Required fororm_select,orm_update,orm_delete.orm_insertworks without a key.
Setup pattern¶
enum PlayerData
{
pId,
pName[MAX_PLAYER_NAME],
Float:pScore,
pLevel
}
new g_player_data[MAX_PLAYERS][PlayerData];
new g_player_orm[MAX_PLAYERS];
stock SetupPlayerORM(playerid)
{
new oid = orm_create("players", g_mysql);
g_player_orm[playerid] = oid;
orm_addvar_int(oid, g_player_data[playerid][pId], "id");
orm_addvar_string(oid, g_player_data[playerid][pName], MAX_PLAYER_NAME, "name");
orm_addvar_float(oid, g_player_data[playerid][pScore], "score");
orm_addvar_int(oid, g_player_data[playerid][pLevel], "level");
orm_setkey(oid, "id");
}
CRUD operations¶
Every CRUD native shares the same signature:
native bool:orm_select(orm_id, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:orm_update(orm_id, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:orm_insert(orm_id, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:orm_delete(orm_id, const callback[] = "", const format[] = "", {Float,_}:...);
native bool:orm_save(orm_id, const callback[] = "", const format[] = "", {Float,_}:...);
All five route through mysql_query (FIFO ordering, non-blocking). Each returns false synchronously when:
orm_idis unknown (MYSQL_ERROR_INVALID_ORM),- the query cannot be built (
MYSQL_ERROR_ORM_KEY_NOT_SETfor select/update/delete,MYSQL_ERROR_INVALID_ORMfor insert/save), - the underlying connection has gone away.
orm_select¶
Generated SQL: SELECT col1, col2, … FROM \table` WHERE `key` =
g_player_data[playerid][pId] = db_id;
orm_select(g_player_orm[playerid], "OnPlayerDataLoaded", "d", playerid);
forward OnPlayerDataLoaded(playerid);
public OnPlayerDataLoaded(playerid)
{
// copy cache row 0 into the bound variables
orm_apply_cache(g_player_orm[playerid]);
if (orm_errno(g_player_orm[playerid]) == ORM_NO_DATA)
{
printf("no row for player %d", playerid);
return;
}
printf("loaded %s (level %d)",
g_player_data[playerid][pName],
g_player_data[playerid][pLevel]);
}
orm_insert¶
Generated SQL: INSERT INTO \table` (col1, col2, …) VALUES (val1, val2, …)`.
g_player_data[playerid][pName] = "NewPlayer";
g_player_data[playerid][pLevel] = 1;
g_player_data[playerid][pScore] = 0.0;
orm_insert(g_player_orm[playerid], "OnPlayerInserted", "d", playerid);
forward OnPlayerInserted(playerid);
public OnPlayerInserted(playerid)
{
g_player_data[playerid][pId] = cache_insert_id();
printf("inserted with id %d", g_player_data[playerid][pId]);
}
orm_update¶
Generated SQL: UPDATE \table` SET col1=val1, col2=val2, … WHERE `key` =
g_player_data[playerid][pLevel] = 10;
g_player_data[playerid][pScore] = 1500.0;
orm_update(g_player_orm[playerid]);
orm_delete¶
Generated SQL: DELETE FROM \table` WHERE `key` =
orm_delete(g_player_orm[playerid], "OnPlayerDeleted", "d", playerid);
forward OnPlayerDeleted(playerid);
public OnPlayerDeleted(playerid)
{
printf("player %d removed", playerid);
}
orm_save¶
Decides between INSERT and UPDATE by looking at the current value of the key column:
- Int key equals
0→ INSERT. - Float key equals
0.0→ INSERT. - String key is empty (
"") → INSERT. - Anything else → UPDATE.
// First save after creation does an INSERT (id is 0), later saves do UPDATE.
orm_save(g_player_orm[playerid], "OnPlayerSaved", "d", playerid);
orm_apply_cache¶
Writes one row of the active cache into the bound variables. Must be called from a query callback, while the cache is active. Returns true on success.
Failure modes:
| Condition | Return | orm_errno |
|---|---|---|
| No cache currently active | false |
unchanged (sets global plugin error to MYSQL_ERROR_NO_CACHE_ACTIVE) |
orm_id is unknown |
false |
unchanged (sets global plugin error to MYSQL_ERROR_INVALID_ORM) |
row is negative or >= cache_get_row_count() |
false |
ORM_NO_DATA |
When a row index is valid, the plugin walks each binding and copies the matching column into the Pawn variable. Bindings whose column is missing from the cache are silently skipped (the Pawn variable keeps its current value).
orm_errno¶
Returns the ORM error code for the last operation on the instance, or -1 if orm_id is unknown.
| Code | Constant | Meaning |
|---|---|---|
| 0 | ORM_OK |
The last orm_apply_cache succeeded |
| 1 | ORM_NO_DATA |
The last orm_apply_cache failed because the requested row does not exist |
This errno is only updated by orm_apply_cache. The threaded CRUD natives (orm_select/insert/update/delete/save) surface their failures synchronously (return false) and through the global mysql_errno(0) / OnQueryError, not through orm_errno.
Generated SQL: safety¶
- String columns are escaped through the same
escape_stringused bymysql_format %s. - Table and column names are sanitized through
escape_identifier, which strips backticks. The output is then wrapped in backticks. - Numeric columns are formatted directly (
{}for ints,{}for floats — no locale-aware formatting).
The result is safe against SQL injection through the bound variables. Do not insert hostile column names through orm_setkey or orm_addvar_*; the plugin removes backticks but does not run a full identifier validator.
End-to-end example¶
#include <a_samp>
#include <mysql_samp>
#define MAX_PLAYER_NAME 24
enum pInfo
{
pDBId,
pName[MAX_PLAYER_NAME],
pLevel,
Float:pMoney
}
new PlayerInfo[MAX_PLAYERS][pInfo];
new PlayerORM[MAX_PLAYERS];
new g_mysql;
public OnGameModeInit()
{
g_mysql = mysql_connect("127.0.0.1", "root", "", "samp_server");
if (mysql_errno() != MYSQL_OK)
{
printf("[MySQL] connection failed");
return 1;
}
return 1;
}
public OnPlayerConnect(playerid)
{
new oid = orm_create("players", g_mysql);
PlayerORM[playerid] = oid;
orm_addvar_int(oid, PlayerInfo[playerid][pDBId], "id");
orm_addvar_string(oid, PlayerInfo[playerid][pName], MAX_PLAYER_NAME, "name");
orm_addvar_int(oid, PlayerInfo[playerid][pLevel], "level");
orm_addvar_float(oid, PlayerInfo[playerid][pMoney], "money");
orm_setkey(oid, "id");
GetPlayerName(playerid, PlayerInfo[playerid][pName], MAX_PLAYER_NAME);
new query[128];
mysql_format(g_mysql, query, sizeof(query),
"SELECT * FROM players WHERE name = '%s' LIMIT 1",
PlayerInfo[playerid][pName]);
mysql_query(g_mysql, query, "OnPlayerLookup", "d", playerid);
return 1;
}
forward OnPlayerLookup(playerid);
public OnPlayerLookup(playerid)
{
if (cache_get_row_count() > 0)
{
orm_apply_cache(PlayerORM[playerid]);
printf("loaded %s (id=%d, level=%d)",
PlayerInfo[playerid][pName],
PlayerInfo[playerid][pDBId],
PlayerInfo[playerid][pLevel]);
}
else
{
// brand-new player
PlayerInfo[playerid][pLevel] = 1;
PlayerInfo[playerid][pMoney] = 0.0;
orm_insert(PlayerORM[playerid], "OnPlayerCreated", "d", playerid);
}
}
forward OnPlayerCreated(playerid);
public OnPlayerCreated(playerid)
{
PlayerInfo[playerid][pDBId] = cache_insert_id();
printf("new player saved with id %d", PlayerInfo[playerid][pDBId]);
}
public OnPlayerDisconnect(playerid, reason)
{
orm_save(PlayerORM[playerid]);
orm_destroy(PlayerORM[playerid]);
return 1;
}