A simple, lightweight, and fast MySQL/SQLite library for Garry's Mod.
β οΈ Note: The latest version requires the next Garry's Mod update (available on dev/x86-64 branch).
- π Features
- π¦ Installation
- π οΈ Quick Setup
- π API Reference
- π§ Configuration Options
- π Examples
- β Dual Database Support - MySQL and SQLite
- β‘ Async & Sync - Both asynchronous and synchronous queries
- π Easy Transactions - Simple transaction handling with coroutines
- π Migration System - Built-in database migrations
- π¦ Single File - One-file library for easy integration
-
Download the latest
goobie-sql.lua
from GitHub Releases -
For MySQL users: Also download
gmsv_goobie_mysql_x_x_x.dll
and extract to:garrysmod/lua/bin/gmsv_goobie_mysql_x_x_x.dll
-
Add
goobie-sql.lua
to your addon'sthirdparty
folder
local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
driver = "sqlite",
})
local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
driver = "mysql",
uri = "mysql://USERNAME:PASSWORD@HOST/DATABASE",
})
Creates and starts a connection automatically. For async initialization, pass a callback as the second argument.
Parameters:
options
(table) - Connection configuration options
Example:
local conn = goobie_sql.NewConn({
driver = "mysql", -- "mysql" or "sqlite"
-- Error handling callback
on_error = function(err, trace)
print("Database error:", err)
end,
-- MySQL Options (choose URI or individual options)
uri = "mysql://user:pass@host:port/db", -- Recommended
-- OR
host = "127.0.0.1",
port = 3306,
username = "root",
password = "password",
database = "mydb",
-- Additional MySQL settings
charset = "utf8mb4",
collation = "utf8mb4_unicode_ci",
timezone = "UTC",
statement_cache_capacity = 100,
socket = "/tmp/mysql.sock",
})
Method | Description | Returns |
---|---|---|
Conn:Start(callback) |
Connect asynchronously | - |
Conn:StartSync() |
Connect synchronously | throws on error |
Conn:Disconnect(callback) |
Disconnect asynchronously | - |
Conn:DisconnectSync() |
Disconnect synchronously | err |
Conn:State() |
Get connection state | number |
Conn:StateName() |
Get connection state name | string |
Conn:ID() |
Get connection ID | number |
Conn:Host() |
Get host | string |
Conn:Port() |
Get port | number |
Conn:Ping(callback) |
Ping database async | - |
Conn:PingSync() |
Ping database sync | err, latency |
Method | Type | Description | Returns |
---|---|---|---|
Conn:Run(query, opts) |
Async | Execute query (no result) | - |
Conn:RunSync(query, opts) |
Sync | Execute query (no result) | err |
Conn:Execute(query, opts) |
Async | Execute with metadata | - |
Conn:ExecuteSync(query, opts) |
Sync | Execute with metadata | err, result |
Conn:Fetch(query, opts) |
Async | Fetch multiple rows | - |
Conn:FetchSync(query, opts) |
Sync | Fetch multiple rows | err, rows |
Conn:FetchOne(query, opts) |
Async | Fetch single row | - |
Conn:FetchOneSync(query, opts) |
Sync | Fetch single row | err, row |
Conn:UpsertQuery(table, opts) |
Async | Insert or update | - |
Conn:UpsertQuerySync(table, opts) |
Sync | Insert or update | err, result |
{
params = {"value1", "value2"}, -- Parameters for placeholders {1}, {2}
callback = function(err, res) end, -- Async callback
raw = false -- Set true for multi-statement queries (no params)
}
Use Begin()
or BeginSync()
for database transactions. Inside transactions, queries return results directly (no callbacks).
-- Async transaction
conn:Begin(function(err, txn)
if err then return end
local err, res = txn:Execute("INSERT INTO users (name) VALUES ('John')")
if err then
txn:Rollback() -- Must rollback explicitly on error
return
end
local err = txn:Commit()
print("Transaction complete, open:", txn:IsOpen()) -- false
end)
-- Sync transaction
local err, txn = conn:BeginSync()
if not err then
local err, res = txn:Execute("INSERT INTO users (name) VALUES ('Jane')")
if err then
txn:Rollback()
else
txn:Commit()
end
end
Run database migrations with version tracking:
local conn = goobie_sql.NewConn({
driver = "sqlite",
addon_name = "my_addon", -- Required for migration tracking
})
local current_version, first_run = conn:RunMigrations({
-- Migration 1: String format
{
UP = [[
CREATE TABLE users (
id {CROSS_PRIMARY_AUTO_INCREMENTED},
name TEXT NOT NULL,
created_at {CROSS_OS_TIME_TYPE}
);
]],
DOWN = "DROP TABLE users;"
},
-- Migration 2: Function format
{
UP = function(process, conn)
process("ALTER TABLE users ADD COLUMN email TEXT;")
end,
DOWN = function(process, conn)
process("ALTER TABLE users DROP COLUMN email;")
end
}
})
print("Database version:", current_version, "First run:", first_run)
{
UP = [[
CREATE TABLE test (
--@ifdef SQLITE
id INTEGER PRIMARY KEY,
--@else
id BIGINT AUTO_INCREMENT PRIMARY KEY,
--@endif
name TEXT
);
]]
}
Write database-agnostic queries using cross-syntax placeholders:
Placeholder | SQLite | MySQL |
---|---|---|
{CROSS_NOW} |
(CAST(strftime('%s', 'now') AS INTEGER)) |
(UNIX_TIMESTAMP()) |
{CROSS_PRIMARY_AUTO_INCREMENTED} |
INTEGER PRIMARY KEY |
BIGINT AUTO_INCREMENT PRIMARY KEY |
{CROSS_COLLATE_BINARY} |
COLLATE BINARY |
BINARY |
{CROSS_CURRENT_DATE} |
DATE('now') |
CURDATE() |
{CROSS_OS_TIME_TYPE} |
INT UNSIGNED NOT NULL DEFAULT (...) |
INT UNSIGNED NOT NULL DEFAULT (...) |
{CROSS_INT_TYPE} |
INTEGER |
BIGINT |
{CROSS_JSON_TYPE} |
TEXT |
JSON |
Example:
conn:RunSync([[
SELECT * FROM users WHERE created_at > {CROSS_NOW}
]])
{
message = "Error description",
code = 1234, -- MySQL error code (optional)
sqlstate = "42000" -- SQL state code (optional)
}
-- Has __tostring metamethod for easy printing
local opts = {
primary_keys = {"id"}, -- Unique/primary keys that could conflict
inserts = { -- Values to insert
id = 1,
name = "John",
email = "[email protected]"
},
updates = {"name", "email"}, -- Columns to update on conflict
binary_columns = {"data"}, -- Binary columns (SQLite specific)
callback = function(err, res) end -- Async callback
}
conn:UpsertQuery("users", opts)
-- OR
local err, res = conn:UpsertQuerySync("users", opts)
-- Simple insert
conn:Execute("INSERT INTO users (name) VALUES ({1})", {
params = {"Alice"},
callback = function(err, res)
if err then
print("Error:", err)
else
print("Inserted ID:", res.last_insert_id)
print("Rows affected:", res.rows_affected)
end
end
})
-- Fetch multiple rows
conn:Fetch("SELECT * FROM users WHERE age > {1}", {
params = {18},
callback = function(err, rows)
if not err then
for i, row in ipairs(rows) do
print("User:", row.name, "Age:", row.age)
end
end
end
})
-- Fetch single row
conn:FetchOne("SELECT * FROM users WHERE id = {1}", {
params = {1},
callback = function(err, user)
if not err and user then
print("Found user:", user.name)
end
end
})
-- Synchronous queries (easier for simple operations)
local err = conn:RunSync("DELETE FROM users WHERE inactive = 1")
if err then
print("Delete failed:", err)
end
local err, users = conn:FetchSync("SELECT * FROM users LIMIT 10")
if not err then
print("Found", #users, "users")
end
conn:Begin(function(err, txn)
if err then return end
-- Transfer money between accounts
local err, sender = txn:FetchOne("SELECT balance FROM accounts WHERE id = {1}", {
params = {sender_id}
})
if err or not sender or sender.balance < amount then
txn:Rollback()
return
end
-- Deduct from sender
local err = txn:Execute("UPDATE accounts SET balance = balance - {1} WHERE id = {2}", {
params = {amount, sender_id}
})
if err then
txn:Rollback()
return
end
-- Add to receiver
local err = txn:Execute("UPDATE accounts SET balance = balance + {1} WHERE id = {2}", {
params = {amount, receiver_id}
})
if err then
txn:Rollback()
return
end
-- Commit transaction
local err = txn:Commit()
if not err then
print("Transfer completed successfully!")
end
end)
-- Complete example with error handling and migrations
local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
driver = "mysql",
uri = "mysql://user:pass@localhost/gamedb",
addon_name = "my_gamemode",
on_error = function(err, trace)
print("[DB Error]", err.message)
if err.code then
print("Error code:", err.code)
end
end
})
-- Run migrations
local version, first_run = conn:RunMigrations({
{
UP = [[
CREATE TABLE players (
steam_id VARCHAR(32) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
playtime {CROSS_INT_TYPE} DEFAULT 0,
created_at {CROSS_OS_TIME_TYPE}
);
]],
DOWN = "DROP TABLE players;"
},
{
UP = "ALTER TABLE players ADD COLUMN last_seen {CROSS_OS_TIME_TYPE};",
DOWN = "ALTER TABLE players DROP COLUMN last_seen;"
}
})
if first_run then
print("Database initialized for the first time!")
else
print("Database updated to version", version)
end
-- Use the connection
conn:UpsertQuery("players", {
primary_keys = {"steam_id"},
inserts = {
steam_id = "STEAM_1:0:123456",
name = "PlayerName",
playtime = 0
},
updates = {"name", "last_seen"}
})
- Documentation: MySQL URI Format
- Issues: GitHub Issues
- Releases: GitHub Releases
π‘ Pro Tip: Ping connections sparingly! Check this article on why frequent connection pinging can be harmful.