Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

121 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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

  • βœ… 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

  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)

local goobie_sql = include("myaddon/thirdparty/goobie-sql.lua")
local conn = goobie_sql.NewConn({
    driver = "sqlite",
})

MySQL

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:

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) 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

Query Methods

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

Query Options

{
    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).

-- 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:

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

{
    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:

conn:RunSync([[
    SELECT * FROM users WHERE created_at > {CROSS_NOW}
]])

πŸ”§ Configuration Options

Error Object Structure

{
    message = "Error description",
    code = 1234, -- MySQL error code (optional)
    sqlstate = "42000" -- SQL state code (optional)
}
-- Has __tostring metamethod for easy printing

UpsertQuery Options

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

-- 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

-- 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

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

-- 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

πŸ’‘ Pro Tip: Ping connections sparingly! Check this article on why frequent connection pinging can be harmful.

About

Easily connect to MySQL in Garry's Mod using Rust binary!

Topics

Resources

Stars

23 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages