-
Notifications
You must be signed in to change notification settings - Fork 0
SQL Dialect
An embeddable SQL database engine for Arma 3 mods. Like SQLite for Arma — a
Rust callExtension that lets modders write SQL directly in SQF.
["CREATE TABLE weapons (id STRING PRIMARY KEY, name STRING)"] call a3db_fnc_execute;
["INSERT INTO weapons VALUES ('m4a1', 'M4A1')"] call a3db_fnc_execute;
_result = ["SELECT * FROM weapons WHERE name %% 'm4'"] call a3db_fnc_execute;| Category | Features |
|---|---|
| SQL | CREATE/DROP TABLE/INDEX, INSERT, SELECT, UPDATE, DELETE, REPLACE INTO, TRUNCATE, RENAME |
| Advanced SQL | JOINs (CROSS/INNER/LEFT), GROUP BY/HAVING, ORDER BY/LIMIT/OFFSET, UNION/UNION ALL, CTE (WITH), subqueries |
| Expressions |
%% fuzzy match, LIKE, BETWEEN, IN, IS NULL, CASE WHEN, EXISTS, CAST |
| Functions | UPPER/LOWER, LENGTH, SUBSTR, TRIM, CONCAT, COALESCE/IFNULL, ROUND, ABS, NOW()/CURRENT_TIMESTAMP |
| Window | ROW_NUMBER, RANK, DENSE_RANK with OVER/PARTITION BY/ORDER BY |
| Constraints | PRIMARY KEY, NOT NULL, DEFAULT, CHECK, FOREIGN KEY, AUTO_INCREMENT |
| Types | INT (BIGINT/SMALLINT/TINYINT), FLOAT (DECIMAL/NUMERIC/DOUBLE), STRING (VARCHAR/CHAR/TEXT), BOOL, DATE/TIMESTAMP, STRINGS[]/FLOATS[] |
| Indices | BTREE (exact/range), TRIGRAM (fuzzy GIN-style candidate filter) |
| Transactions | BEGIN/COMMIT/ROLLBACK, SAVEPOINT/RELEASE |
| Persistence | SAVE/LOAD (binary), export/import JSON/CSV/SQL, export_to_file |
| Security | Parameterized queries ($1,$2), TCP LOGIN auth, CBA credential settings |
| Network | TCP listener (auto-start at game boot), external queries via Python/CLI |
| Multi-statement | Run ;-separated SQL batches |
| Multi-dialect | Accepts PostgreSQL, MySQL/MariaDB, SQLite, DataFusion-style SQL |
In your mod's CfgPatches:
requiredAddons[] = {"cba_main", "a3db_main", "a3db_sql"};// Create
["CREATE TABLE players (uid STRING PRIMARY KEY, name STRING, score INT)"] call a3db_fnc_execute;
// Insert
["INSERT INTO players VALUES ('76561198000000001', 'Scarface', 1500)"] call a3db_fnc_execute;
// Query
_result = ["SELECT name, score FROM players WHERE score > 1000 ORDER BY score DESC"] call a3db_fnc_execute;
// Returns: [0, "OK", [["name","score"],["Scarface",1500]]]// Fuzzy search
_result = ["SELECT name FROM weapons WHERE name %% 'm4'"] call a3db_fnc_execute;
// Transactions
["BEGIN"] call a3db_fnc_execute;
["INSERT INTO log (action) VALUES ('mission_start')"] call a3db_fnc_execute;
["COMMIT"] call a3db_fnc_execute;
// Save/load persistence
["data.bin"] call a3db_fnc_save;
["data.bin"] call a3db_fnc_load;
// Export
_table_data = ["players"] call a3db_fnc_exportJSON;
_sql_backup = [] call a3db_fnc_exportSQL;
// External TCP query (from Python)
// ["listen"] call a3db_fnc_execute; // auto-starts at game bootOptions → Addon Configuration → A3DB:
| Setting | Type | Default | Purpose |
|---|---|---|---|
| Enable TCP Listener | CHECKBOX | true | Auto-start on game boot |
| Listener Port | EDIT | 33306 | TCP port |
| Listener Bind Address | EDIT | 127.0.0.1 | Bind IP |
| Listener Username | EDIT | (empty) | TCP login (empty = anonymous) |
| Listener Password | EDIT | (empty) | TCP login |
| Auto-Save | CHECKBOX | false | Save on mission end |
| Auto-Load | CHECKBOX | false | Load on mission start |
| Auto-Save Path | EDIT | a3db_autosave.bin | File path |
| Log Level | LIST | INFO | RPT verbosity |
- Auto-Save: Save database when mission ends
- Auto-Save File: File path for auto-save
if (isServer) then {
// Create tables on mission start
["CREATE TABLE IF NOT EXISTS stats (uid STRING, name STRING, score INT)"] call a3db_fnc_execute;
// Restore from previous session
["stats_data.bin"] call a3db_fnc_load;
// Auto-save on mission end
addMissionEventHandler ["Ended", {
["stats_data.bin"] call a3db_fnc_save;
}];
};
// Record event
["INSERT INTO stats VALUES ('76561198000000001', 'Scarface', 1500)"] call a3db_fnc_execute;
// Query top scores
_result = ["SELECT name, score FROM stats ORDER BY score DESC LIMIT 10"] call a3db_fnc_execute;Enable the TCP listener in CBA settings, then connect from any tool:
import socket
s = socket.socket()
s.connect(("127.0.0.1", 33306))
s.sendall(b"SELECT * FROM stats ORDER BY score DESC LIMIT 5\n")
print(s.recv(65536).decode())
s.close()-- Tables
CREATE TABLE weapons (id STRING PRIMARY KEY, name STRING, caliber STRING, barrelLength FLOAT);
CREATE TABLE attachments (id STRING PRIMARY KEY, weaponId STRING, name STRING, mass FLOAT);
-- Types: STRING, INT, FLOAT, BOOL, STRINGS[], FLOATS[]
-- CRUD
INSERT INTO weapons VALUES ('rhs_m4a1', 'M4A1', '5.56x45mm', 368.3);
SELECT * FROM weapons WHERE caliber = '5.56x45mm';
SELECT name, caliber FROM weapons WHERE barrelLength > 400.0 ORDER BY barrelLength DESC;
UPDATE weapons SET caliber = '7.62x39mm' WHERE id = 'ak74';
DELETE FROM weapons WHERE barrelLength IS NULL;
DROP TABLE weapons;
-- Fuzzy match (trigram similarity)
SELECT * FROM weapons WHERE id %% 'rhs_m4';
-- matches rhs_m4a1, rhs_m4a1_carryhandle, etc.
-- JOINS
SELECT w.name, a.name FROM weapons w INNER JOIN attachments a ON w.id = a.weaponId;
SELECT * FROM weapons w LEFT JOIN attachments a ON w.id = a.weaponId;
-- Aggregates
SELECT COUNT(*) FROM weapons;
SELECT AVG(barrelLength) FROM weapons;
SELECT caliber, COUNT(*) AS cnt FROM weapons GROUP BY caliber;
-- Ordering & limits
SELECT * FROM weapons ORDER BY name ASC LIMIT 10 OFFSET 5;
-- Transactions
BEGIN;
INSERT INTO weapons VALUES ('test', 'Test', '9x19mm', 200.0);
ROLLBACK; -- or COMMIT
-- Savepoints
SAVEPOINT sp1;
INSERT INTO weapons VALUES ('tmp', 'Temp', '5.56x45mm', 300.0);
ROLLBACK TO sp1;
RELEASE SAVEPOINT sp1;
-- Indices
CREATE INDEX idx_caliber ON weapons (caliber) USING BTREE;
CREATE INDEX idx_name_fuzzy ON weapons (name) USING TRIGRAM;
-- REPLACE / UPSERT
REPLACE INTO weapons VALUES ('m4a1', 'M4A1', '5.56x45mm', 368.3);
-- ALTER TABLE
ALTER TABLE weapons ADD COLUMN mass FLOAT;
ALTER TABLE weapons DROP COLUMN barrelLength;
ALTER TABLE weapons RENAME COLUMN name TO displayName;
ALTER TABLE weapons RENAME TO armory;
-- TRUNCATE
TRUNCATE TABLE weapons;
-- Window functions
SELECT id, name, ROW_NUMBER() OVER (ORDER BY name) AS rn FROM weapons;
SELECT id, name, RANK() OVER (PARTITION BY caliber ORDER BY name) FROM weapons;
-- Subqueries
SELECT * FROM weapons WHERE id IN (SELECT weaponId FROM attachments);
SELECT * FROM weapons WHERE EXISTS (SELECT 1 FROM attachments WHERE weaponId = weapons.id);
-- CTE
WITH top AS (SELECT * FROM weapons ORDER BY name LIMIT 5) SELECT * FROM top;
-- CAST
SELECT CAST(barrelLength AS INT) FROM weapons;
SELECT name || ' (' || caliber || ')' AS combined FROM weapons;
-- Functions
SELECT NOW(), CURRENT_TIMESTAMP;
SELECT COALESCE(barrelLength, 0.0) FROM weapons;
SELECT UPPER(name), LOWER(name), LENGTH(name), SUBSTR(name, 1, 3) FROM weapons;// In init.sqf or CfgFunctions init:
private _version = "a3db" callExtension "version";
diag_log text format ["[A3DB] Loading: %1", _version];// Single SQL statement (STRING callExtension STRING):
private _result = "a3db" callExtension "SELECT * FROM weapons";
// SQL with args (STRING callExtension ARRAY):
private _result = ["a3db", "INSERT INTO weapons VALUES ('m4a1', 'M4A1', '5.56x45mm', 368.3)"] callExtension;
// Multi-statement (separate with semicolons):
private _result = "a3db" callExtension "CREATE TABLE t (id STRING); INSERT INTO t VALUES ('a'); SELECT * FROM t";[returnCode, status, data]
Success: [0,"OK",result_data]
Error: [-1,"ERR_CODE","error message"]
Error codes:
ERR_PARSE SQL parse error
ERR_EXEC Execution error
ERR_TABLE Table not found
ERR_TYPE Type mismatch
ERR_PK Primary key violation
ERR_IO File I/O error
ERR_INTERNAL Internal error
result_data for SELECT is a JSON array of column names + rows:
[["id","name","caliber","barrelLength"],["rhs_m4a1","M4A1","5.56x45mm",368.3]]For JOINs with prefixed column names:
[["weapons.id","weapons.name","attachments.name"],["rhs_m4a1","M4A1","M68 CCO"]]// Version
private _version = "a3db" callExtension "version";
// → [0,"OK","a3db 0.1.0"]
// SQL dump
private _dump = "a3db" callExtension "dump_sql";
// → [0,"OK","CREATE TABLE weapons (...);..."]
// Query with parameterized args (SQL injection safe)
private _result = ["a3db", "SELECT * FROM weapons WHERE id = $1", ["m4a1"]] callExtension;
// TCP listener (auto-starts on game boot). Manual control:
private _result = ["a3db", "listen", ["33306"]] callExtension;
private _result = ["a3db", "stop"] callExtension;When using CBA (recommended), the addon registers these functions via CfgFunctions:
| Function | Description |
|---|---|
a3db_fnc_init |
Initialize extension, returns version string |
a3db_fnc_execute |
Execute SQL, returns parsed result |
a3db_fnc_loadJSON |
Import JSON data into a table |
a3db_fnc_dumpSQL |
Export full database as SQL dump |
a3db_fnc_exportJSON |
Export table as JSON |
a3db_fnc_exportCSV |
Export table as CSV |
a3db_fnc_exportSQL |
Export full database as SQL statements |
a3db_fnc_save |
Persist database to binary file |
a3db_fnc_load |
Restore database from binary file |
a3db_fnc_init |
Initialize extension |
a3db_fnc_settings |
Register CBA settings (auto-called via PreInit) |
a3db_fnc_postInit |
Post-mission init (auto-save/load hooks) |
Prevent SQL injection by passing user input as separate args with $1, $2 placeholders:
// UNSAFE — string interpolation (SQL injection possible)
private _sql = format ["SELECT * FROM users WHERE name = '%1'", _userInput];
["a3db", _sql] callExtension;
// SAFE — parameterized query (injection prevented)
["a3db", "SELECT * FROM users WHERE name = $1", [_userInput]] callExtension;Set a username and password in CBA Settings (Options → Addon Configuration → A3DB).
When credentials are non-empty, clients must LOGIN before querying:
import socket
s = socket.socket()
s.connect(("127.0.0.1", 33306))
s.sendall(b"LOGIN admin mypassword\n")
print(s.recv(65536).decode()) # [0,"OK","Authenticated"]
s.sendall(b"SELECT * FROM weapons\n")
print(s.recv(65536).decode())
s.close()-
Rust 1.80+ (for
std::sync::LazyLock) - HEMTT 1.20+ — Arma 3 addon build tool
- C++ build tools (for Rust cross-compilation to Windows)
- MinGW-w64 (for Windows cross-compilation on Linux):
sudo apt-get install mingw-w64 gcc-multilib
# Build for Linux x86_64
cargo build --release
# Build for Windows (cross-compile from Linux)
cargo build --release --target x86_64-pc-windows-gnu
cargo build --release --target i686-pc-windows-gnuhemtt buildOutput goes to .hemttout/build/.
# All 118 tests (0.01s)
cargo test --lib -p a3db# Rust
cargo fmt --check
cargo clippy --all-targets
# SQF
sqflint addons/a3db/*.sqf
sqfvm --parse-only -i addons/a3db/fn_init.sqf
# Arma addon structure
hemtt checkThe project includes a GitHub Actions workflow (.github/workflows/ci.yml) that:
- Runs
cargo test - Builds for 4 targets:
x86_64-linux,i686-linux,x86_64-windows,i686-windows - Runs
hemtt buildto produce the addon PBOs - On release: creates a
a3db-<tag>.zipwith the complete mod
Test locally with ACT:
act -j test # Run test job
act --list # List all jobsa3db/
├── Cargo.toml # Workspace → extension/
├── extension/ # Rust extension crate (cdylib + rlib)
│ ├── Cargo.toml
│ ├── .cargo/config.toml # Cross-compilation linkers
│ └── src/
│ ├── lib.rs # C ABI (RVExtension, RVExtensionArgs, RVExtensionVersion)
│ ├── parser/ # SQL parser (sqlparser-rs + custom A3DbDialect)
│ │ ├── dialect.rs # A3DbDialect (GenericDialect-based, multi-dialect)
│ │ ├── preprocessor.rs # %% → fuzzy_match(), string-literal-aware
│ │ └── mod.rs # parse_sql() entry point
│ └── engine/ # In-memory database engine
│ ├── database.rs # Table storage + transaction snapshots
│ ├── table.rs # Row/column storage, CRUD, trigram similarity
│ ├── value.rs # ColumnType, Column, DbValue enums
│ ├── execute.rs # Statement executor + expression evaluator + JOINs
│ ├── index.rs # BTreeIndex + TrigramIndex (GIN-style)
│ ├── serialize.rs # JSON, CSV, SQL dump, Binary formats
│ └── error.rs # Structured error codes (ERR_*)
├── addons/
│ ├── main/ # Main addon (CBA macro includes + CfgPatches)
│ │ ├── config.cpp
│ │ ├── script_mod.hpp
│ │ └── $PBOPREFIX$
│ └── sql/ # SQL engine addon (CfgFunctions + SQF API)
│ ├── config.cpp
│ ├── script_component.hpp
│ ├── fn_init.sqf
│ ├── fn_settings.sqf
│ ├── fn_postInit.sqf
│ ├── fn_execute.sqf
│ ├── fn_loadJSON.sqf
│ ├── fn_dumpSQL.sqf
│ ├── fn_exportJSON.sqf
│ ├── fn_exportCSV.sqf
│ ├── fn_exportSQL.sqf
│ ├── fn_save.sqf
│ ├── fn_load.sqf
│ └── $PBOPREFIX$
├── include/
│ └── x/cba/addons/ # CBA header stubs for build-time resolution
│ ├── main/
│ │ ├── script_mod.hpp
│ │ ├── script_macros.hpp
│ │ └── script_macros_common.hpp
│ └── xeh/
│ └── script_xeh.hpp
├── .hemtt/
│ └── project.toml # HEMTT v1 build config
├── .github/workflows/ci.yml # GitHub Actions CI/CD
├── mod.cpp # Mod definition (name, logo, etc.)
├── meta.cpp # Steam Workshop metadata (publishedid)
├── tools/ # Development utility scripts
│ ├── build_current_addon.py
│ ├── config_style_checker.py
│ ├── getExtensionHash.py
│ ├── search_privates.py
│ ├── search_unused_privates.py
│ └── sqfvmChecker.py
└── README.md
Built following the same conventions as ACE3 and CBA_A3:
| Aspect | Convention |
|---|---|
| Prefix |
prefix = "a3db", mainprefix = "z"
|
| PBO path | z\a3db\addons\{addon_name} |
| Include path | \z\a3db\addons\main\script_mod.hpp |
| CBA dependency | CBA_A3 required (cba_main, cba_xeh) |
| Build system | HEMTT v1 (.hemtt/project.toml) |
| Rust workspace | Workspace at root, crate in extension/
|
| Release profile |
opt-level = "z", lto = true, strip = true
|
MIT — use freely in your Arma 3 mods.