-
Notifications
You must be signed in to change notification settings - Fork 0
Module Guide
Documentation for modders and scripters who want to store, share, and query data using a3sql.
- 1. Adding a3sql as a dependency
- 2. Function reference
- 3. Schema registry convention
- 4. Dependency injection pattern
- 5. Security model
- 6. Best practices
- 7. Example: simple tracking mod
Add a3sql_main and a3sql_database to your mod's requiredAddons[]:
class CfgPatches {
class MyMod {
requiredAddons[] = {"a3sql_main", "a3sql_database", "cba_xeh"};
};
};| Dependency | What it provides |
|---|---|
a3sql_main |
Core extension load, CBA settings, version check |
a3sql_database |
SQF wrapper functions (a3sql_fnc_*) |
cba_xeh |
Extended Event Handlers (init, preInit, postInit) |
If you use the patch framework, add a3sql_patch_core too:
requiredAddons[] = {"a3sql_database", "a3sql_patch_core", "cba_xeh"};All functions take an optional extension name as the last parameter (defaults to "a3sql"). They return a parsed result array [code, "status", data] where code == 0 means success.
| Function | Description |
|---|---|
a3sql_fnc_execute |
Run SQL. Supports parameterized queries via $1, $2
|
a3sql_fnc_executePrepared |
Run a prepared statement by name with params |
a3sql_fnc_executeTimed |
Same as execute, logs to RPT if query takes >10ms |
a3sql_fnc_prepare |
Prepare a named statement for repeated use |
a3sql_fnc_selectAll |
SELECT with auto-pagination for large result sets |
a3sql_fnc_selectArray |
SELECT returning rows as arrays (skips column headers) |
a3sql_fnc_selectMap |
SELECT returning rows as hash maps (column name -> value) |
| Function | Description |
|---|---|
a3sql_fnc_save |
Save full database to binary file |
a3sql_fnc_load |
Restore full database from binary file |
a3sql_fnc_exportJSON |
Export a table as JSON |
a3sql_fnc_exportCSV |
Export a table as CSV |
a3sql_fnc_exportSQL |
Export the full database as SQL dump |
a3sql_fnc_loadJSON |
Load JSON data into a table |
a3sql_fnc_dumpSQL |
Alias for exportSQL
|
| Function | Description |
|---|---|
a3sql_fnc_init |
Initialize extension, print version to RPT |
a3sql_fnc_settings |
Register CBA settings (called automatically via PreInit) |
a3sql_fnc_postInit |
Set up auto-save/load hooks (called automatically) |
// Execute a statement
_result = ["CREATE TABLE IF NOT EXISTS players (uid STRING PRIMARY KEY, name STRING, score INT)"] call a3sql_fnc_execute;
// Query with results as hash maps
_result = ["SELECT name, score FROM players ORDER BY score DESC"] call a3sql_fnc_selectMap;
// Returns: [{name: "Scarface", score: 1500}, {name: "Stitch", score: 1200}]
// Parameterized query (safe)
_result = ["SELECT * FROM players WHERE uid = $1", "a3sql", ["76561198000000001"]] call a3sql_fnc_execute;
// Prepared statement
["get_player", "SELECT name, score FROM players WHERE uid = $1"] call a3sql_fnc_prepare;
_result = ["get_player", ["76561198000000001"]] call a3sql_fnc_executePrepared;
// Persistence
["mydata.bin"] call a3sql_fnc_save;
["mydata.bin"] call a3sql_fnc_load;The project follows ACE3 conventions with modular, single-responsibility addons:
| Addon | Function prefix | Purpose |
|---|---|---|
a3sql_admin |
a3sql_admin_fnc_* |
Server command execution + player tracking |
a3sql_analytics |
a3sql_analytics_fnc_* |
Perf monitoring, kill/shoot events, replay snapshots |
a3sql_database |
a3sql_fnc_* |
Core SQL execution, persistence, export |
a3sql_loadouts |
a3sql_loadouts_fnc_* |
Faction/role loadout templates |
a3sql_main |
— | Core defines, version, macros |
a3sql_patch_core |
a3sql_patch_core_fnc_* |
Dynamic patching engine, handlers |
a3sql_patch_editor |
a3sql_patch_editor_fnc_* |
In-game rule editor UI |
a3sql_patch_operators |
a3sql_patch_operators_fnc_* |
Value transformer operators |
a3sql_persistence |
a3sql_persistence_fnc_* |
Player state save/restore on DC/JIP |
a3sql_progression |
a3sql_progression_fnc_* |
Rank/score bridge via existing Arma systems |
a3sql uses an in-process, in-memory database. Every mod that writes to the extension shares the same database namespace. There are no separate databases per mod, so tables must be namespaced to avoid collisions.
Prefix your tables with a short mod identifier:
["CREATE TABLE IF NOT EXISTS mytracker_events (
id INTEGER PRIMARY KEY,
event_type TEXT,
pos_x FLOAT,
pos_y FLOAT,
mission_name TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"] call a3sql_fnc_execute;Two mods both creating CREATE TABLE events will collide. The second CREATE TABLE fails. If they use CREATE TABLE IF NOT EXISTS, the second silently
skips and the first mod's schema wins, which might not match what the second
mod expects.
- Choose a short prefix:
mythical_,my_,tm_,abc_ - Keep it lowercase, no spaces
- Document your tables so other mods can read them
a3sql's shared database enables a powerful pattern: Mod A writes data, Mod B reads it without either mod depending on the other.
Example:
- Mod A (Telemetry) writes to
mod_telemetry_events - Mod B (Live Map) reads from
mod_telemetry_eventsand displays on a web dashboard - Both mods only depend on
a3sql_database - Neither mod knows the other exists
This decouples mod dependencies at the config level while still allowing data sharing at runtime.
// Mod A writes
["INSERT INTO mod_telemetry_events (event_type, pos_x, pos_y) VALUES ('shot', 100, 200)"] call a3sql_fnc_execute;
// Mod B reads
_result = ["SELECT * FROM mod_telemetry_events WHERE event_type = 'shot'"] call a3sql_fnc_selectMap;When your mod writes data that others consume, treat your table schema as a public API:
- Document the table columns and types
- Version your table names if you expect schema changes:
mod_events_v2 - Add a
_schematable or comment convention for discovery
If you want to provide a friendlier API on top of raw SQL, wrap queries in your own functions:
// mymod_fnc_getTopPlayers.sqf
params ["_limit"];
_result = [format ["SELECT name, score FROM mymod_stats ORDER BY score DESC LIMIT %1", _limit]] call a3sql_fnc_selectMap;
_resultAlways use $1, $2 placeholders for user-supplied values:
// Safe
_result = ["SELECT * FROM players WHERE name = $1", "a3sql", [_playerInput]] call a3sql_fnc_execute;
// Unsafe - do not use string interpolation for user input
_result = [format ["SELECT * FROM players WHERE name = '%1'", _playerInput]] call a3sql_fnc_execute;The TCP listener (enabled by default, port 33306) accepts SQL queries from external tools:
- Binds to
127.0.0.1by default (localhost only) - Set a username and password in CBA settings to require
LOGINbefore queries - Change the bind address to
0.0.0.0only if you need remote access across a network - The extension runs in-process with the game and has access to the game's in-memory database
- Localhost listener: any process on the same machine can connect
- Remote listener: any process that can reach the port can connect
- Amend/credentialed access: the listener authenticates before accepting SQL
-
SQL injection: prevented by using
$1parameterized syntax - Mod collisions: no isolation between mod databases (all in one process space)
- Mission SQF: any mod loaded on the server can write to any table (no per-mod access control in a3sql's in-memory database)
- Keep the listener bound to
127.0.0.1unless you need remote queries - Always set credentials for the TCP listener in production
- Use parameterized queries for any user-supplied input
- Do not expose the TCP port to the public internet
- Sync traffic should use SSH tunnels if crossing untrusted networks
- Always use
CREATE TABLE IF NOT EXISTSto handle re-insertion - Use
INTEGER PRIMARY KEYfor auto-incrementing IDs - Use prepared statements for user input:
SELECT * FROM table WHERE uid = $1 - Use
IF EXISTSbeforeDROP TABLE - Clean up old data:
DELETE FROM events_shots WHERE created_at < datetime('now', '-30 days')— SQLite-style modifiers supported - Batch INSERTs when importing large datasets: use multi-VALUES format
- Use
LIMIT+OFFSETfor pagination when SELECTing many rows - Run analytics/report queries during mission end, not mid-game
- Use
call a3sql_fnc_selectMapinstead ofcall a3sql_fnc_executefor easier data access
class CfgPatches {
class MyTracker {
name = "My Tracker";
author = "Me";
requiredVersion = 2.02;
requiredAddons[] = {"a3sql_database", "cba_xeh"};
units[] = {};
weapons[] = {};
};
};#include "script_component.hpp"
if (!isServer) exitWith {};
// Create the events table
private _sql = "CREATE TABLE IF NOT EXISTS mytracker_events (
id INTEGER PRIMARY KEY,
event_type TEXT NOT NULL,
pos_x FLOAT DEFAULT 0.0,
pos_y FLOAT DEFAULT 0.0,
mission_name TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)";
_sql call a3sql_fnc_execute;
// Record player connections
addMissionEventHandler ["PlayerConnected", {
params ["_id", "_uid", "_name", "_jip", "_owner"];
["INSERT INTO mytracker_events (event_type, pos_x, pos_y, mission_name) VALUES ('connect', 0, 0, $1)", "a3sql", [missionName]] call a3sql_fnc_execute;
}];
// Clean up old events every 10 minutes — datetime() supports SQLite-style
// modifiers: '+1 day', '-30 days', '+3 hours', etc.
[{
["DELETE FROM mytracker_events WHERE created_at < datetime('now', '-7 days')"] call a3sql_fnc_execute;
}, [], 600] call CBA_fnc_addPerFrameHandler;#include "script_component.hpp"
// Query top 5 players by score
_result = ["SELECT uid, name, score FROM player_progression ORDER BY score DESC LIMIT 5"] call a3sql_fnc_selectMap;
{
diag_log text format ["[MyTracker] Player %1 (%2): %3", _x get "name", _x get "uid", _x get "score"];
} forEach _result;
_result