https://github.com/srlion/goobie-sql
Easily connect to MySQL in Garry's Mod using Rust binary!
https://github.com/srlion/goobie-sql
async binary fast garrysmod gmod gmod-mysql graceful graceful-shutdown mysql rust rust-lang rustlang simple sql transactions
Last synced: 2 months ago
JSON representation
Easily connect to MySQL in Garry's Mod using Rust binary!
- Host: GitHub
- URL: https://github.com/srlion/goobie-sql
- Owner: Srlion
- License: mit
- Created: 2024-10-06T09:35:08.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2025-03-11T10:06:47.000Z (about 1 year ago)
- Last Synced: 2025-03-14T10:01:44.493Z (about 1 year ago)
- Topics: async, binary, fast, garrysmod, gmod, gmod-mysql, graceful, graceful-shutdown, mysql, rust, rust-lang, rustlang, simple, sql, transactions
- Language: Lua
- Homepage:
- Size: 101 KB
- Stars: 10
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Goobie SQL
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).
## 📋 Table of Contents
- [🚀 Features](#-features)
- [📦 Installation](#-installation)
- [🛠️ Quick Setup](#️-quick-setup)
- [📚 API Reference](#-api-reference)
- [Connection Setup](#connection-setup)
- [Connection Management](#connection-management)
- [Query Methods](#query-methods)
- [Transactions](#transactions)
- [Migrations](#migrations)
- [Cross Syntaxes](#cross-syntaxes)
- [🔧 Configuration Options](#-configuration-options)
- [📄 Examples](#-examples)
## 🚀 Features
- ✅ **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
## 📦 Installation
1. Download the latest `goobie-sql.lua` from [GitHub Releases](https://github.com/Srlion/goobie-sql/releases/latest)
2. **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
```
3. Add `goobie-sql.lua` to your addon's `thirdparty` folder
## 🛠️ Quick Setup
### SQLite (Recommended for beginners)
```lua
local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
driver = "sqlite",
})
```
### MySQL
```lua
local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
driver = "mysql",
uri = "mysql://USERNAME:PASSWORD@HOST/DATABASE",
})
```
## 📚 API Reference
### Connection Setup
#### `goobie_sql.NewConn(options)`
Creates and starts a connection automatically. For async initialization, pass a callback as the second argument.
**Parameters:**
- `options` (table) - Connection configuration options
**Example:**
```lua
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",
})
```
### Connection Management
| Method | Description | Returns |
|--------|-------------|---------|
| [`Conn:Start(callback)`](#connstart) | Connect asynchronously | - |
| [`Conn:StartSync()`](#connstartsync) | Connect synchronously | throws on error |
| [`Conn:Disconnect(callback)`](#conndisconnect) | Disconnect asynchronously | - |
| [`Conn:DisconnectSync()`](#conndisconnectsync) | Disconnect synchronously | `err` |
| [`Conn:State()`](#connstate) | Get connection state | `number` |
| [`Conn:StateName()`](#connstatename) | Get connection state name | `string` |
| [`Conn:ID()`](#connid) | Get connection ID | `number` |
| [`Conn:Host()`](#connhost) | Get host | `string` |
| [`Conn:Port()`](#connport) | Get port | `number` |
| [`Conn:Ping(callback)`](#connping) | Ping database async | - |
| [`Conn:PingSync()`](#connpingsync) | Ping database sync | `err, latency` |
### Query Methods
| Method | Type | Description | Returns |
|--------|------|-------------|---------|
| [`Conn:Run(query, opts)`](#connrun) | Async | Execute query (no result) | - |
| [`Conn:RunSync(query, opts)`](#connrunsync) | Sync | Execute query (no result) | `err` |
| [`Conn:Execute(query, opts)`](#connexecute) | Async | Execute with metadata | - |
| [`Conn:ExecuteSync(query, opts)`](#connexecutesync) | Sync | Execute with metadata | `err, result` |
| [`Conn:Fetch(query, opts)`](#connfetch) | Async | Fetch multiple rows | - |
| [`Conn:FetchSync(query, opts)`](#connfetchsync) | Sync | Fetch multiple rows | `err, rows` |
| [`Conn:FetchOne(query, opts)`](#connfetchone) | Async | Fetch single row | - |
| [`Conn:FetchOneSync(query, opts)`](#connfetchonesync) | Sync | Fetch single row | `err, row` |
| [`Conn:UpsertQuery(table, opts)`](#connupsertquery) | Async | Insert or update | - |
| [`Conn:UpsertQuerySync(table, opts)`](#connupsertquerysync) | Sync | Insert or update | `err, result` |
#### Query Options
```lua
{
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)
}
```
### Transactions
Use `Begin()` or `BeginSync()` for database transactions. Inside transactions, queries return results directly (no callbacks).
```lua
-- 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
```
### Migrations
Run database migrations with version tracking:
```lua
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)
```
#### Conditional Migrations
```lua
{
UP = [[
CREATE TABLE test (
--@ifdef SQLITE
id INTEGER PRIMARY KEY,
--@else
id BIGINT AUTO_INCREMENT PRIMARY KEY,
--@endif
name TEXT
);
]]
}
```
### Cross Syntaxes
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:**
```lua
conn:RunSync([[
SELECT * FROM users WHERE created_at > {CROSS_NOW}
]])
```
## 🔧 Configuration Options
### Error Object Structure
```lua
{
message = "Error description",
code = 1234, -- MySQL error code (optional)
sqlstate = "42000" -- SQL state code (optional)
}
-- Has __tostring metamethod for easy printing
```
### UpsertQuery Options
```lua
local opts = {
primary_keys = {"id"}, -- Unique/primary keys that could conflict
inserts = { -- Values to insert
id = 1,
name = "John",
email = "john@example.com"
},
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)
```
## 📄 Examples
### Basic Query Examples
```lua
-- 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 Examples
```lua
-- 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
```
### Advanced Transaction Example
```lua
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 Setup Example
```lua
-- 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"}
})
```
---
## 📞 Support
- **Documentation:** [MySQL URI Format](https://docs.rs/sqlx/latest/sqlx/mysql/struct.MySqlConnectOptions.html)
- **Issues:** [GitHub Issues](https://github.com/Srlion/goobie-sql/issues)
- **Releases:** [GitHub Releases](https://github.com/Srlion/goobie-sql/releases)
> 💡 **Pro Tip:** Ping connections sparingly! Check [this article](https://www.percona.com/blog/checking-for-a-live-database-connection-considered-harmful/) on why frequent connection pinging can be harmful.