520 lines
19 KiB
Lua
520 lines
19 KiB
Lua
--[[
|
|
MySQLite - Abstraction mechanism for SQLite and MySQL
|
|
|
|
Why use this?
|
|
- Easy to use interface for MySQL
|
|
- No need to modify code when switching between SQLite and MySQL
|
|
- Queued queries: execute a bunch of queries in order an run the callback when all queries are done
|
|
|
|
License: LGPL V2.1 (read here: https://www.gnu.org/licenses/lgpl-2.1.html)
|
|
|
|
Supported MySQL modules:
|
|
- MySQLOO
|
|
- tmysql4
|
|
|
|
Note: When both MySQLOO and tmysql4 modules are installed, MySQLOO is used by default.
|
|
|
|
/*---------------------------------------------------------------------------
|
|
Documentation
|
|
---------------------------------------------------------------------------*/
|
|
|
|
MySQLite.initialize([config :: table]) :: No value
|
|
Initialize MySQLite. Loads the config from either the config parameter OR the MySQLite_config global.
|
|
This loads the module (if necessary) and connects to the MySQL database (if set up).
|
|
The config must have this layout:
|
|
{
|
|
EnableMySQL :: Bool - set to true to use MySQL, false for SQLite
|
|
Host :: String - database hostname
|
|
Username :: String - database username
|
|
Password :: String - database password (keep away from clients!)
|
|
Database_name :: String - name of the database
|
|
Database_port :: Number - connection port (3306 by default)
|
|
Preferred_module :: String - Preferred module, case sensitive, must be either "mysqloo" or "tmysql4"
|
|
MultiStatements :: Bool - Only available in tmysql4: allow multiple SQL statements per query
|
|
}
|
|
|
|
----------------------------- Utility functions -----------------------------
|
|
MySQLite.isMySQL() :: Bool
|
|
Returns whether MySQLite is set up to use MySQL. True for MySQL, false for SQLite.
|
|
Use this when the query syntax between SQLite and MySQL differs (example: AUTOINCREMENT vs AUTO_INCREMENT)
|
|
|
|
MySQLite.SQLStr(str :: String) :: String
|
|
Escapes the string and puts it in quotes.
|
|
It uses the escaping method of the module that is currently being used.
|
|
|
|
MySQLite.tableExists(tbl :: String, callback :: function, errorCallback :: function)
|
|
Checks whether table tbl exists.
|
|
|
|
callback format: function(res :: Bool)
|
|
res is a boolean indicating whether the table exists.
|
|
|
|
The errorCallback format is the same as in MySQLite.query.
|
|
|
|
----------------------------- Running queries -----------------------------
|
|
MySQLite.query(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
|
Runs a query. Calls the callback parameter when finished, calls errorCallback when an error occurs.
|
|
|
|
callback format:
|
|
function(result :: table, lastInsert :: number)
|
|
Result is the table with results (nil when there are no results or when the result list is empty)
|
|
lastInsert is the row number of the last inserted value (use with AUTOINCREMENT)
|
|
|
|
Note: lastInsert is NOT supported when using SQLite.
|
|
|
|
errorCallback format:
|
|
function(error :: String, query :: String) :: Bool
|
|
error is the error given by the database module.
|
|
query is the query that triggered the error.
|
|
|
|
Return true to suppress the error!
|
|
|
|
MySQLite.queryValue(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
|
Runs a query and returns the first value it comes across.
|
|
|
|
callback format:
|
|
function(result :: any)
|
|
where the result is either a string or a number, depending on the requested database field.
|
|
|
|
The errorCallback format is the same as in MySQLite.query.
|
|
|
|
MySQLite.prepare(sqlText :: String, sqlParams :: Table, callback :: function, errorCallback :: function)
|
|
Calls a prepared statement, faster for queries that are ran multiple times. Params do not need to be escaped.
|
|
This does not work with SQLite, and will instead call a regular query
|
|
|
|
callback format:
|
|
function(result :: table, lastInsert :: number, rowsChanged :: number)
|
|
Result is the table with results (nil when there are no results or when the result list is empty)
|
|
lastInsert is the row number of the last inserted value (use with AUTOINCREMENT)
|
|
rowsChanged is the amount of rows that were changed with the query
|
|
|
|
Note: lastInsert is NOT supported when using SQLite.
|
|
|
|
The errorCallback format is the same as in MySQLite.query.
|
|
|
|
Example:
|
|
MySQLite.prepare("UPDATE `darkrp_player` SET `rpname` = ? WHERE `rpname` = ?", { "players_old_name", "players_new_name"}, success_callback, error_callback)
|
|
|
|
----------------------------- Transactions -----------------------------
|
|
MySQLite.begin() :: No value
|
|
Starts a transaction. Use in combination with MySQLite.queueQuery and MySQLite.commit.
|
|
|
|
MySQLite.queueQuery(sqlText :: String, callback :: function, errorCallback :: function) :: No value
|
|
Queues a query in the transaction. Note: a transaction must be started with MySQLite.begin() for this to work.
|
|
The callback will be called when this specific query has been executed successfully.
|
|
The errorCallback function will be called when an error occurs in this specific query.
|
|
|
|
See MySQLite.query for the callback and errorCallback format.
|
|
|
|
MySQLite.commit(onFinished)
|
|
Commits a transaction and calls onFinished when EVERY queued query has finished.
|
|
onFinished is NOT called when an error occurs in one of the queued queries.
|
|
|
|
onFinished is called without arguments.
|
|
|
|
----------------------------- Hooks -----------------------------
|
|
DatabaseInitialized
|
|
Called when a successful connection to the database has been made.
|
|
]]
|
|
|
|
local debug = debug
|
|
local error = error
|
|
local ErrorNoHalt = ErrorNoHalt
|
|
local hook = hook
|
|
local pairs = pairs
|
|
local require = require
|
|
local sql = sql
|
|
local string = string
|
|
local table = table
|
|
local timer = timer
|
|
local tostring = tostring
|
|
local mysqlOO
|
|
local TMySQL
|
|
local _G = _G
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local unpack = unpack
|
|
|
|
local multistatements
|
|
|
|
local MySQLite_config = MySQLite_config or RP_MySQLConfig or FPP_MySQLConfig
|
|
local moduleLoaded
|
|
|
|
local function loadMySQLModule()
|
|
if moduleLoaded or not MySQLite_config or not MySQLite_config.EnableMySQL then return end
|
|
|
|
local moo, tmsql = util.IsBinaryModuleInstalled("mysqloo"), util.IsBinaryModuleInstalled("tmysql4")
|
|
|
|
if not moo and not tmsql then
|
|
error("Could not find a suitable MySQL module. Please either:\n - Install tmysql. It can be obtained from https://github.com/SuperiorServers/gm_tmysql4\n - Install MySQLOO. It can be obtained from https://github.com/FredyH/MySQLOO\nDue to this error, MySQL is disabled. This means that SQLite is used instead to store data.")
|
|
end
|
|
moduleLoaded = true
|
|
|
|
require(
|
|
moo and tmsql and MySQLite_config.Preferred_module or
|
|
moo and "mysqloo" or
|
|
tmsql and "tmysql4"
|
|
)
|
|
|
|
multistatements = CLIENT_MULTI_STATEMENTS
|
|
|
|
mysqlOO = mysqloo
|
|
TMySQL = tmysql
|
|
|
|
if MySQLite_config.Preferred_module == "tmysql4" then
|
|
if not tmsql then
|
|
ErrorNoHalt("The preferred module for MySQL is selected to be tmysql4. However, tmysql4 does not appear to be installed. Please either:\n - Install tmysql. It can be obtained from https://github.com/SuperiorServers/gm_tmysql4\n - Select MySQLOO as the preferred module for MySQL. MySQLOO appears to be installed.")
|
|
return
|
|
end
|
|
|
|
if not tmysql.Version or tmysql.Version < 4.1 then
|
|
MsgC(Color(255, 0, 0), "Using older tmysql version, please consider updating!\n")
|
|
MsgC(Color(255, 0, 0), "Newer Version: https://github.com/SuperiorServers/gm_tmysql4\n")
|
|
end
|
|
|
|
-- Turns tmysql.Connect into tmysql.Initialize if they're using an older version.
|
|
TMySQL.Connect = tmysql.Version and tmysql.Version >= 4.1 and TMySQL.Connect or TMySQL.initialize
|
|
TMySQL.SetOption = tmysql.Version and tmysql.Version >= 4.1 and TMySQL.SetOption or TMySQL.Option
|
|
else
|
|
if not moo then
|
|
ErrorNoHalt("The preferred module for MySQL is selected to be MySQLOO. However, MySQLOO does not appear to be installed. Please either:\n - Install MySQLOO. It can be obtained from https://github.com/FredyH/MySQLOO\n - Select tmysql4 as the preferred module for MySQL. tmysql4 appears to be installed.")
|
|
end
|
|
end
|
|
end
|
|
loadMySQLModule()
|
|
|
|
module("MySQLite")
|
|
|
|
-- Helper function to return the first value found when iterating over a table.
|
|
-- Replaces the now deprecated table.GetFirstValue
|
|
local function arbitraryTableValue(tbl)
|
|
for _, v in pairs(tbl) do return v end
|
|
end
|
|
|
|
function initialize(config)
|
|
MySQLite_config = config or MySQLite_config
|
|
|
|
if not MySQLite_config then
|
|
ErrorNoHalt("Warning: No MySQL config!")
|
|
end
|
|
|
|
loadMySQLModule()
|
|
|
|
if MySQLite_config.EnableMySQL then
|
|
connectToMySQL(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
|
else
|
|
timer.Simple(0, function()
|
|
_G.GAMEMODE.DatabaseInitialized = _G.GAMEMODE.DatabaseInitialized or function() end
|
|
hook.Call("DatabaseInitialized", _G.GAMEMODE)
|
|
end)
|
|
end
|
|
end
|
|
|
|
local CONNECTED_TO_MYSQL = false
|
|
local msOOConnect
|
|
databaseObject = nil
|
|
|
|
local queuedQueries
|
|
local cachedQueries
|
|
|
|
function isMySQL()
|
|
return CONNECTED_TO_MYSQL
|
|
end
|
|
|
|
function begin()
|
|
if not CONNECTED_TO_MYSQL then
|
|
sql.Begin()
|
|
else
|
|
if queuedQueries then
|
|
debug.Trace()
|
|
error("Transaction ongoing!")
|
|
end
|
|
queuedQueries = {}
|
|
end
|
|
end
|
|
|
|
function commit(onFinished)
|
|
if not CONNECTED_TO_MYSQL then
|
|
sql.Commit()
|
|
if onFinished then onFinished() end
|
|
return
|
|
end
|
|
|
|
if not queuedQueries then
|
|
error("No queued queries! Call begin() first!")
|
|
end
|
|
|
|
if #queuedQueries == 0 then
|
|
queuedQueries = nil
|
|
if onFinished then onFinished() end
|
|
return
|
|
end
|
|
|
|
-- Copy the table so other scripts can create their own queue
|
|
local queue = table.Copy(queuedQueries)
|
|
queuedQueries = nil
|
|
|
|
-- Handle queued queries in order
|
|
local queuePos = 0
|
|
local call
|
|
|
|
-- Recursion invariant: queuePos > 0 and queue[queuePos] <= #queue
|
|
call = function(...)
|
|
queuePos = queuePos + 1
|
|
|
|
if queue[queuePos].callback then
|
|
queue[queuePos].callback(...)
|
|
end
|
|
|
|
-- Base case, end of the queue
|
|
if queuePos + 1 > #queue then
|
|
if onFinished then onFinished() end -- All queries have finished
|
|
return
|
|
end
|
|
|
|
-- Recursion
|
|
local nextQuery = queue[queuePos + 1]
|
|
query(nextQuery.query, call, nextQuery.onError)
|
|
end
|
|
|
|
query(queue[1].query, call, queue[1].onError)
|
|
end
|
|
|
|
function queueQuery(sqlText, callback, errorCallback)
|
|
if CONNECTED_TO_MYSQL then
|
|
table.insert(queuedQueries, {query = sqlText, callback = callback, onError = errorCallback})
|
|
return
|
|
end
|
|
-- SQLite is instantaneous, simply running the query is equal to queueing it
|
|
query(sqlText, callback, errorCallback)
|
|
end
|
|
|
|
local function msOOQuery(sqlText, callback, errorCallback, queryValue)
|
|
local queryObject = databaseObject:query(sqlText)
|
|
local data
|
|
queryObject.onData = function(Q, D)
|
|
data = data or {}
|
|
data[#data + 1] = D
|
|
end
|
|
|
|
queryObject.onError = function(Q, E)
|
|
if databaseObject:status() == mysqlOO.DATABASE_NOT_CONNECTED then
|
|
table.insert(cachedQueries, {sqlText, callback, queryValue})
|
|
|
|
-- Immediately try reconnecting
|
|
msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
|
return
|
|
end
|
|
|
|
local supp = errorCallback and errorCallback(E, sqlText)
|
|
if not supp then error(E .. " (" .. sqlText .. ")") end
|
|
end
|
|
|
|
queryObject.onSuccess = function()
|
|
local res = queryValue and data and data[1] and arbitraryTableValue(data[1]) or not queryValue and data or nil
|
|
if callback then callback(res, queryObject:lastInsert()) end
|
|
end
|
|
queryObject:start()
|
|
end
|
|
|
|
local preparedStatements = {}
|
|
local paramTypes = {}
|
|
paramTypes["number"] = function(queryObj, paramIndex, paramValue) return queryObj:setNumber(paramIndex, paramValue) end
|
|
paramTypes["string"] = function(queryObj, paramIndex, paramValue) return queryObj:setString(paramIndex, paramValue) end
|
|
paramTypes["boolean"] = function(queryObj, paramIndex, paramValue) return queryObj:setBoolean(paramIndex, paramValue) end
|
|
|
|
local function msOOPrepare(sqlText, sqlParams, callback, errorCallback)
|
|
local queryObject
|
|
|
|
if preparedStatements[sqlText] then
|
|
queryObject = preparedStatements[sqlText]
|
|
else
|
|
queryObject = databaseObject:prepare(sqlText)
|
|
preparedStatements[sqlText] = queryObject
|
|
end
|
|
|
|
for i, param in ipairs(sqlParams) do
|
|
local paramType = type(param)
|
|
|
|
if paramTypes[paramType] then
|
|
paramTypes[paramType](queryObject, i, param)
|
|
else
|
|
queryObject:setString(i, param)
|
|
end
|
|
end
|
|
|
|
queryObject.onError = function(_, E)
|
|
local supp = errorCallback and errorCallback(E, sqlText)
|
|
if not supp then error(E .. " (" .. sqlText .. ")") end
|
|
end
|
|
|
|
queryObject.onSuccess = function(_, data)
|
|
if callback then callback(data, queryObject:lastInsert(), queryObject:affectedRows()) end
|
|
end
|
|
|
|
queryObject:start()
|
|
end
|
|
|
|
local function tmsqlPrepare(sqlText, sqlParams, callback, errorCallback)
|
|
local queryObject
|
|
|
|
if preparedStatements[sqlText] then
|
|
queryObject = preparedStatements[sqlText]
|
|
else
|
|
queryObject = databaseObject:Prepare(sqlText)
|
|
preparedStatements[sqlText] = queryObject
|
|
end
|
|
|
|
for i, param in ipairs(sqlParams) do
|
|
param = SQLStr(param, true)
|
|
end
|
|
|
|
local varcount = queryObject:GetArgCount()
|
|
|
|
sqlParams[varcount + 1] = function(results)
|
|
if results[1].error ~= nil then
|
|
local supp = errorCallback and errorCallback(E, results[1].error)
|
|
if not supp then error(E .. " (" .. results[1].error .. ")") end
|
|
end
|
|
|
|
if callback then callback(data, results[1].lastid, results[1].affected) end
|
|
end
|
|
|
|
queryObject:Run(unpack(sqlParams, 1, varcount + 2))
|
|
end
|
|
|
|
local function tmsqlQuery(sqlText, callback, errorCallback, queryValue)
|
|
local call = function(res)
|
|
res = res[1] -- For now only support one result set
|
|
if not res.status then
|
|
local supp = errorCallback and errorCallback(res.error, sqlText)
|
|
if not supp then error(res.error .. " (" .. sqlText .. ")") end
|
|
return
|
|
end
|
|
|
|
if not res.data or #res.data == 0 then res.data = nil end -- compatibility with other backends
|
|
if queryValue and callback then return callback(res.data and res.data[1] and arbitraryTableValue(res.data[1]) or nil) end
|
|
if callback then callback(res.data, res.lastid) end
|
|
end
|
|
|
|
databaseObject:Query(sqlText, call)
|
|
end
|
|
|
|
local function SQLiteQuery(sqlText, callback, errorCallback, queryValue)
|
|
sql.m_strError = "" -- reset last error
|
|
|
|
local lastError = sql.LastError()
|
|
local Result = queryValue and sql.QueryValue(sqlText) or sql.Query(sqlText)
|
|
|
|
if sql.LastError() and sql.LastError() ~= lastError then
|
|
local err = sql.LastError()
|
|
local supp = errorCallback and errorCallback(err, sqlText)
|
|
if supp == false then error(err .. " (" .. sqlText .. ")", 2) end
|
|
return
|
|
end
|
|
|
|
if callback then callback(Result) end
|
|
return Result
|
|
end
|
|
|
|
-- SQLite doesn't support preparedStatements, so convert it to a regular query.
|
|
local function SQLitePrepare(sqlText, sqlParams, callback, errorCallback)
|
|
for _, param in ipairs(sqlParams) do
|
|
sqlText = sqlText:gsub("%?", sql.SQLStr(param), 1)
|
|
end
|
|
|
|
return SQLiteQuery(sqlText, callback, errorCallback, false)
|
|
end
|
|
|
|
function query(sqlText, callback, errorCallback)
|
|
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
|
|
return qFunc(sqlText, callback, errorCallback, false)
|
|
end
|
|
|
|
function queryValue(sqlText, callback, errorCallback)
|
|
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOQuery) or (TMySQL and tmsqlQuery))) or SQLiteQuery
|
|
return qFunc(sqlText, callback, errorCallback, true)
|
|
end
|
|
|
|
function prepare(sqlText, sqlParams, callback, errorCallback)
|
|
local qFunc = (CONNECTED_TO_MYSQL and ((mysqlOO and msOOPrepare) or (TMySQL and tmsqlPrepare))) or SQLitePrepare
|
|
return qFunc(sqlText, sqlParams, callback, errorCallback, false)
|
|
end
|
|
|
|
local function onConnected()
|
|
CONNECTED_TO_MYSQL = true
|
|
|
|
-- Run the queries that were called before the connection was made
|
|
for k, v in pairs(cachedQueries or {}) do
|
|
cachedQueries[k] = nil
|
|
if v[3] then
|
|
queryValue(v[1], v[2])
|
|
else
|
|
query(v[1], v[2])
|
|
end
|
|
end
|
|
cachedQueries = {}
|
|
local GM = _G.GAMEMODE or _G.GM
|
|
|
|
hook.Call("DatabaseInitialized", GM.DatabaseInitialized and GM or nil)
|
|
|
|
end
|
|
|
|
msOOConnect = function(host, username, password, database_name, database_port)
|
|
databaseObject = mysqlOO.connect(host, username, password, database_name, database_port)
|
|
|
|
if timer.Exists("darkrp_check_mysql_status") then timer.Remove("darkrp_check_mysql_status") end
|
|
|
|
databaseObject.onConnectionFailed = function(_, msg)
|
|
timer.Simple(5, function()
|
|
msOOConnect(MySQLite_config.Host, MySQLite_config.Username, MySQLite_config.Password, MySQLite_config.Database_name, MySQLite_config.Database_port)
|
|
end)
|
|
error("Connection failed! " .. tostring(msg) .. "\nTrying again in 5 seconds.")
|
|
end
|
|
|
|
databaseObject.onConnected = onConnected
|
|
|
|
databaseObject:connect()
|
|
end
|
|
|
|
local function tmsqlConnect(host, username, password, database_name, database_port)
|
|
local db, err = TMySQL.Connect(host, username, password, database_name, database_port, nil, MySQLite_config.MultiStatements and multistatements or nil)
|
|
if err then error("Connection failed! " .. err .. "\n") end
|
|
|
|
databaseObject = db
|
|
onConnected()
|
|
|
|
if (TMySQL.Version and TMySQL.Version >= 4.1) then
|
|
hook.Add("Think", "MySQLite:tmysqlPoll", function()
|
|
db:Poll()
|
|
end)
|
|
end
|
|
end
|
|
|
|
function connectToMySQL(host, username, password, database_name, database_port)
|
|
database_port = database_port or 3306
|
|
local func = mysqlOO and msOOConnect or TMySQL and tmsqlConnect or function() end
|
|
func(host, username, password, database_name, database_port)
|
|
end
|
|
|
|
function SQLStr(sqlStr)
|
|
local escape =
|
|
not CONNECTED_TO_MYSQL and sql.SQLStr or
|
|
mysqlOO and function(str) return "\"" .. databaseObject:escape(tostring(str)) .. "\"" end or
|
|
TMySQL and function(str) return "\"" .. databaseObject:Escape(tostring(str)) .. "\"" end
|
|
|
|
return escape(sqlStr)
|
|
end
|
|
|
|
function tableExists(tbl, callback, errorCallback)
|
|
if not CONNECTED_TO_MYSQL then
|
|
local exists = sql.TableExists(tbl)
|
|
callback(exists)
|
|
|
|
return exists
|
|
end
|
|
|
|
queryValue(string.format("SHOW TABLES LIKE %s", SQLStr(tbl)), function(v)
|
|
callback(v ~= nil)
|
|
end, errorCallback)
|
|
end
|