diff --git a/minetest.conf b/minetest.conf
index c2b73b7..589b231 100755
--- a/minetest.conf
+++ b/minetest.conf
@@ -24,7 +24,8 @@ debug_log_level = action
strict_protocol_version_checking = false
secure.trusted_mods = mysql_auth,mysql_base,sql,irc,stacktraceplus,multiskin
secure.enable_security = false
-mysql_auth.cfgfile = /home/minetestservers/.minetest/worlds/UGXmr/mysql_auth.cfgfile
+mysql_auth.cfgfile = /home/minetestservers/.minetest/worlds/UGXmr/mysql.cfgfile
+mysql_base.cfgfile = /home/minetestservers/.minetest/worlds/UGXmr/mysql.cfgfile
#
#################Game##################
#
diff --git a/worldmods/mysql_auth/README.md b/worldmods/mysql_auth/README.md
new file mode 100644
index 0000000..8de3bc4
--- /dev/null
+++ b/worldmods/mysql_auth/README.md
@@ -0,0 +1,123 @@
+# mysql_auth
+
+Plug Minetest's auth mechanism into a MySQL database.
+
+# Configuration
+
+First, if mod security is enabled (`secure.enable_security = true`), this mod must be added as
+a trusted mod (in the `secure.trusted_mods` config entry). There is **no** other solution to
+make it work under mod security.
+
+By default `mysql_auth` doesn't run in singleplayer. This can be overriden by setting
+`mysql_auth.enable_singleplayer` to `true`.
+
+Configuration may be done as regular Minetest settings entries, or using a config file, allowing
+for more configuration options; to do so specify the path as `mysql_auth.cfgfile`. This config
+must contain a Lua table that can be read by `minetest.deserialize`, i.e. a regular table
+definition follwing a `return` statement (see the example below).
+
+When using flat Minetest configuation entries, all the following option names must be prefixed
+with `mysql_auth.`. When using a config file, entries are to be hierarchised as per the dot
+separator.
+
+Values written next to option names are default values.
+
+## Database connection
+
+### Flat config file
+
+```lua
+db.host = 'localhost'
+db.user = nil -- MySQL connector defaults to current username
+db.pass = nil -- Using password: NO
+db.port = nil -- MySQL connector defaults to either 3306, or no port if using localhost/unix socket
+db.db = nil -- <== Setting this is required
+```
+
+### Lua table config file
+
+Connection options are passed as a table through the `db.connopts` entry.
+Its format must be the same as [LuaPower's MySQL module `mysql.connect(options_t)` function][mycn],
+that is (all members are optional);
+
+```lua
+connopts = {
+ host = ...,
+ user = ...,
+ pass = ...,
+ db = ...,
+ port = ...,
+ unix_socket = ...,
+ flags = { ... },
+ options = { ... },
+ attrs = { ... },
+ -- Also key, cert, ca, cpath, cipher
+}
+```
+
+## Auth table schema finetuning
+
+```lua
+db.tables.auths.name = 'auths'
+db.tables.auths.schema.userid = 'userid'
+db.tables.auths.schema.userid_type = 'INT'
+db.tables.auths.schema.username = 'username'
+db.tables.auths.schema.username_type = 'VARCHAR(32)'
+db.tables.auths.schema.password = 'password'
+db.tables.auths.schema.password_type = 'VARCHAR(512)'
+db.tables.auths.schema.privs = 'privs'
+db.tables.auths.schema.privs_type = 'VARCHAR(512)'
+db.tables.auths.schema.lastlogin = 'lastlogin'
+db.tables.auths.schema.lastlogin_type = 'BIGINT'
+```
+
+The `_type` config entries are only used when creating an auth table, i.e. when
+`db.tables.auths.name` doesn't exist.
+
+## Examples
+
+### Example 1
+
+#### Using a Lua config file
+
+`minetest.conf`:
+```
+mysql_auth.cfgfile = /srv/minetest/skyblock/mysql_auth_config
+```
+
+`/srv/minetest/skyblock/mysql_auth_config`:
+```lua
+return {
+ db = {
+ connopts = {
+ user = 'minetest',
+ pass = 'BQy77wK$Um6es3Bi($iZ*w3N',
+ db = 'minetest'
+ },
+ tables = {
+ auths = {
+ name = 'skyblock_auths'
+ }
+ }
+ }
+}
+```
+
+#### Using only Minetest config entries
+
+`minetest.conf`:
+```
+mysql_auth.db.user = minetest
+mysql_auth.db.pass = BQy77wK$Um6es3Bi($iZ*w3N
+mysql_auth.db.db = minetest
+mysql_auth.db.tables.auth.name = skyblock_auths
+```
+
+# License
+
+`mysql_auth` is licensed under [LGPLv3](https://www.gnu.org/licenses/lgpl.html).
+
+Using the Public Domain-licensed LuaPower `mysql` module.
+
+
+[mycn]: https://luapower.com/mysql#mysql.connectoptions_t---conn
diff --git a/worldmods/mysql_auth/auth_txt_import.lua b/worldmods/mysql_auth/auth_txt_import.lua
new file mode 100644
index 0000000..d65b55d
--- /dev/null
+++ b/worldmods/mysql_auth/auth_txt_import.lua
@@ -0,0 +1,47 @@
+local modname = minetest.get_current_modname()
+local modpath = minetest.get_modpath(modname)
+
+local thismod = _G[modname]
+
+function thismod.import_auth_txt()
+ minetest.log('action', modname .. ": Importing auth.txt")
+ local auth_file_path = minetest.get_worldpath() .. '/auth.txt'
+ local create_auth_stmt = thismod.create_auth_stmt
+ local create_auth_params = thismod.create_auth_params
+ local conn = thismod.conn
+ local file, errmsg = io.open(auth_file_path, 'rb')
+ if not file then
+ minetest.log('action', modname .. ": " .. auth_file_path .. " could not be opened for reading" ..
+ "(" .. errmsg .. "); no auth entries imported")
+ return
+ end
+ conn:query('SET autocommit=0')
+ conn:query('START TRANSACTION')
+ for line in file:lines() do
+ if line ~= "" then
+ local fields = line:split(":", true)
+ local name, password, privilege_string, last_login = unpack(fields)
+ last_login = tonumber(last_login)
+ if not (name and password and privilege_string) then
+ minetest.log('warning', modname .. ": Invalid line in auth.txt, skipped: " .. dump(line))
+ end
+ minetest.log('info', modname .. " importing player '"..name.."'")
+ create_auth_params:set(1, name)
+ create_auth_params:set(2, password)
+ create_auth_params:set(3, privilege_string)
+ create_auth_params:set(4, last_login)
+ local success, msg = pcall(create_auth_stmt.exec, create_auth_stmt)
+ if not success then
+ error(modname .. ": import failed: " .. msg)
+ end
+ if create_auth_stmt:affected_rows() ~= 1 then
+ error(modname .. ": create_auth failed: affected row count is " ..
+ create_auth_stmt:affected_rows() .. ", expected 1")
+ end
+ end
+ end
+ conn:query('COMMIT')
+ conn:query('SET autocommit=1')
+ io.close(file)
+ minetest.log('action', modname .. ": Finished importing auth.txt")
+end
diff --git a/worldmods/mysql_auth/depends.txt b/worldmods/mysql_auth/depends.txt
new file mode 100644
index 0000000..2170244
--- /dev/null
+++ b/worldmods/mysql_auth/depends.txt
@@ -0,0 +1,2 @@
+mysql_base
+
diff --git a/worldmods/mysql_auth/init.lua b/worldmods/mysql_auth/init.lua
new file mode 100644
index 0000000..5d85313
--- /dev/null
+++ b/worldmods/mysql_auth/init.lua
@@ -0,0 +1,299 @@
+local modname = minetest.get_current_modname()
+local modpath = minetest.get_modpath(modname)
+
+local thismod = {
+ enabled = false
+}
+_G[modname] = thismod
+
+if not mysql_base.enabled then
+ minetest.log('action', modname .. ": mysql_base disabled, not loading mod")
+ return
+end
+
+local singleplayer = minetest.is_singleplayer() -- Caching is OK since you can't open a game to
+-- multiplayer unless you restart it.
+if not minetest.setting_get(modname .. '.enable_singleplayer') and singleplayer then
+ minetest.log('action', modname .. ": Not adding auth handler because of singleplayer game")
+ return
+end
+
+enabled = true
+
+do
+ local get = mysql_base.mkget(modname)
+
+ local conn, dbname = mysql_base.conn, mysql_base.dbname
+
+ local tables = {}
+ do -- Tables and schema settings
+ local t_auths = get('db.tables.auths')
+ if type(t_auths) == 'table' then
+ tables.auths = t_auths
+ else
+ tables.auths = {}
+ tables.auths.name = get('db.tables.auths.name')
+ tables.auths.schema = {}
+ local S = tables.auths.schema
+ S.userid = get('db.tables.auths.schema.userid')
+ S.username = get('db.tables.auths.schema.username')
+ S.password = get('db.tables.auths.schema.password')
+ S.privs = get('db.tables.auths.schema.privs')
+ S.lastlogin = get('db.tables.auths.schema.lastlogin')
+ S.userid_type = get('db.tables.auths.schema.userid_type')
+ S.username_type = get('db.tables.auths.schema.username_type')
+ S.password_type = get('db.tables.auths.schema.password_type')
+ S.privs_type = get('db.tables.auths.schema.privs_type')
+ S.lastlogin_type = get('db.tables.auths.schema.lastlogin_type')
+ end
+
+ do -- Default values
+ tables.auths.name = tables.auths.name or 'auths'
+ tables.auths.schema = tables.auths.schema or {}
+ local S = tables.auths.schema
+ S.userid = S.userid or 'userid'
+ S.username = S.username or 'username'
+ S.password = S.password or 'password'
+ S.privs = S.privs or 'privs'
+ S.lastlogin = S.lastlogin or 'lastlogin'
+
+ S.userid_type = S.userid_type or 'INT'
+ S.username_type = S.username_type or 'VARCHAR(32)'
+ S.password_type = S.password_type or 'VARCHAR(512)'
+ S.privs_type = S.privs_type or 'VARCHAR(512)'
+ S.lastlogin_type = S.lastlogin_type or 'BIGINT'
+ -- Note lastlogin doesn't use the TIMESTAMP type, which is 32-bit and therefore
+ -- subject to the year 2038 problem.
+ end
+ end
+
+ local auth_table_created
+ -- Auth table existence check and setup
+ if not mysql_base.table_exists(tables.auths.name) then
+ -- Auth table doesn't exist, create it
+ local S = tables.auths.schema
+ conn:query('CREATE TABLE ' .. tables.auths.name .. ' (' ..
+ S.userid .. ' ' .. S.userid_type .. ' NOT NULL AUTO_INCREMENT,' ..
+ S.username .. ' ' .. S.username_type .. ' NOT NULL,' ..
+ S.password .. ' ' .. S.password_type .. ' NOT NULL,' ..
+ S.privs .. ' ' .. S.privs_type .. ' NOT NULL,' ..
+ S.lastlogin .. ' ' .. S.lastlogin_type .. ',' ..
+ 'PRIMARY KEY (' .. S.userid .. '),' ..
+ 'UNIQUE (' .. S.username .. ')' ..
+ ')')
+ minetest.log('action', modname .. " created table '" .. dbname .. "." .. tables.auths.name ..
+ "'")
+ auth_table_created = true
+ end
+
+ local S = tables.auths.schema
+ local get_auth_stmt = conn:prepare('SELECT ' .. S.userid .. ',' .. S.password .. ',' .. S.privs ..
+ ',' .. S.lastlogin .. ' FROM ' .. tables.auths.name .. ' WHERE ' .. S.username .. '=?')
+ thismod.get_auth_stmt = get_auth_stmt
+ local get_auth_params = get_auth_stmt:bind_params({S.username_type})
+ thismod.get_auth_params = get_auth_params
+ local get_auth_results = get_auth_stmt:bind_result({S.userid_type, S.password_type, S.privs_type,
+ S.lastlogin_type})
+ thismod.get_auth_results = get_auth_results
+
+ local create_auth_stmt = conn:prepare('INSERT INTO ' .. tables.auths.name .. '(' .. S.username ..
+ ',' .. S.password .. ',' .. S.privs .. ',' .. S.lastlogin .. ') VALUES (?,?,?,?)')
+ thismod.create_auth_stmt = create_auth_stmt
+ local create_auth_params = create_auth_stmt:bind_params({S.username_type, S.password_type,
+ S.privs_type, S.lastlogin_type})
+ thismod.create_auth_params = create_auth_params
+
+ local delete_auth_stmt = conn:prepare('DELETE FROM ' .. tables.auths.name .. ' WHERE ' ..
+ S.username .. '=?')
+ thismod.delete_auth_stmt = delete_auth_stmt
+ local delete_auth_params = delete_auth_stmt:bind_params({S.username_type})
+ thismod.delete_auth_params = delete_auth_params
+
+ local set_password_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' .. S.password ..
+ '=? WHERE ' .. S.username .. '=?')
+ thismod.set_password_stmt = set_password_stmt
+ local set_password_params = set_password_stmt:bind_params({S.password_type, S.username_type})
+ thismod.set_password_params = set_password_params
+
+ local set_privileges_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' .. S.privs ..
+ '=? WHERE ' .. S.username .. '=?')
+ thismod.set_privileges_stmt = set_privileges_stmt
+ local set_privileges_params = set_privileges_stmt:bind_params({S.privs_type, S.username_type})
+ thismod.set_privileges_params = set_privileges_params
+
+ local record_login_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' ..
+ S.lastlogin .. '=? WHERE ' .. S.username .. '=?')
+ thismod.record_login_stmt = record_login_stmt
+ local record_login_params = record_login_stmt:bind_params({S.lastlogin_type, S.username_type})
+ thismod.record_login_params = record_login_params
+
+ local enumerate_auths_query = 'SELECT ' .. S.username .. ',' .. S.password .. ',' .. S.privs ..
+ ',' .. S.lastlogin .. ' FROM ' .. tables.auths.name
+ thismod.enumerate_auths_query = enumerate_auths_query
+
+ if auth_table_created and get('import_auth_txt_on_table_create') ~= 'false' then
+ if not thismod.import_auth_txt then
+ dofile(modpath .. '/auth_txt_import.lua')
+ end
+ thismod.import_auth_txt()
+ end
+
+ thismod.auth_handler = {
+ get_auth = function(name)
+ assert(type(name) == 'string')
+ get_auth_params:set(1, name)
+ local success, msg = pcall(get_auth_stmt.exec, get_auth_stmt)
+ if not success then
+ minetest.log('error', modname .. ": get_auth(" .. name .. ") failed: " .. msg)
+ return nil
+ end
+ get_auth_stmt:store_result()
+ if not get_auth_stmt:fetch() then
+ -- No such auth row exists
+ return nil
+ end
+ while get_auth_stmt:fetch() do
+ minetest.log('warning', modname .. ": get_auth(" .. name .. "): multiples lines were" ..
+ " returned")
+ end
+ local userid, password, privs_str, lastlogin = get_auth_results:get(1),
+ get_auth_results:get(2), get_auth_results:get(3), get_auth_results:get(4)
+ local admin = (name == minetest.setting_get("name"))
+ local privs
+ if singleplayer or admin then
+ privs = {}
+ -- If admin, grant all privs, if singleplayer, grant all privs w/ give_to_singleplayer
+ for priv, def in pairs(core.registered_privileges) do
+ if (singleplayer and def.give_to_singleplayer) or admin then
+ privs[priv] = true
+ end
+ end
+ if admin and not thismod.admin_get_auth_called then
+ thismod.admin_get_auth_called = true
+ thismod.auth_handler.set_privileges(name, privs)
+ end
+ else
+ privs = minetest.string_to_privs(privs_str)
+ end
+ return {
+ userid = userid,
+ password = password,
+ privileges = privs,
+ last_login = tonumber(lastlogin)
+ }
+ end,
+ create_auth = function(name, password, reason)
+ assert(type(name) == 'string')
+ assert(type(password) == 'string')
+ minetest.log('info', modname .. " creating player '"..name.."'" .. (reason or ""))
+ create_auth_params:set(1, name)
+ create_auth_params:set(2, password)
+ create_auth_params:set(3, minetest.setting_get("default_privs"))
+ create_auth_params:set(4, math.floor(os.time()))
+ local success, msg = pcall(create_auth_stmt.exec, create_auth_stmt)
+ if not success then
+ minetest.log('error', modname .. ": create_auth(" .. name .. ") failed: " .. msg)
+ return false
+ end
+ if create_auth_stmt:affected_rows() ~= 1 then
+ minetest.log('error', modname .. ": create_auth(" .. name .. ") failed: affected row" ..
+ " count is " .. create_auth_stmt:affected_rows() .. ", expected 1")
+ return false
+ end
+ return true
+ end,
+ delete_auth = function(name)
+ assert(type(name) == 'string')
+ minetest.log('info', modname .. " deleting player '"..name.."'")
+ delete_auth_params:set(1, name)
+ local success, msg = pcall(delete_auth_stmt.exec, delete_auth_stmt)
+ if not success then
+ minetest.log('error', modname .. ": delete_auth(" .. name .. ") failed: " .. msg)
+ return false
+ end
+ if delete_auth_stmt:affected_rows() ~= 1 then
+ minetest.log('error', modname .. ": delete_auth(" .. name .. ") failed: affected row" ..
+ " count is " .. delete_auth_stmt:affected_rows() .. ", expected 1")
+ return false
+ end
+ return true
+ end,
+ set_password = function(name, password)
+ assert(type(name) == 'string')
+ assert(type(password) == 'string')
+ if not thismod.auth_handler.get_auth(name) then
+ return thismod.auth_handler.create_auth(name, password, " because set_password was requested")
+ else
+ minetest.log('info', modname .. " setting password of player '" .. name .. "'")
+ set_password_params:set(1, password)
+ set_password_params:set(2, name)
+ local success, msg = pcall(set_password_stmt.exec, set_password_stmt)
+ if not success then
+ minetest.log('error', modname .. ": set_password(" .. name .. ") failed: " .. msg)
+ return false
+ end
+ if set_password_stmt:affected_rows() ~= 1 then
+ minetest.log('error', modname .. ": set_password(" .. name .. ") failed: affected row" ..
+ " count is " .. set_password_stmt:affected_rows() .. ", expected 1")
+ return false
+ end
+ return true
+ end
+ end,
+ set_privileges = function(name, privileges)
+ assert(type(name) == 'string')
+ assert(type(privileges) == 'table')
+ set_privileges_params:set(1, minetest.privs_to_string(privileges))
+ set_privileges_params:set(2, name)
+ local success, msg = pcall(set_privileges_stmt.exec, set_privileges_stmt)
+ if not success then
+ minetest.log('error', modname .. ": set_privileges(" .. name .. ") failed: " .. msg)
+ return false
+ end
+ minetest.notify_authentication_modified(name)
+ return true
+ end,
+ reload = function()
+ return true
+ end,
+ record_login = function(name)
+ assert(type(name) == 'string')
+ record_login_params:set(1, math.floor(os.time()))
+ record_login_params:set(2, name)
+ local success, msg = pcall(record_login_stmt.exec, record_login_stmt)
+ if not success then
+ minetest.log('error', modname .. ": record_login(" .. name .. ") failed: " .. msg)
+ return false
+ end
+ if record_login_stmt:affected_rows() ~= 1 then
+ minetest.log('error', modname .. ": record_login(" .. name .. ") failed: affected row" ..
+ " count is " .. record_login_stmt:affected_rows() .. ", expected 1")
+ return false
+ end
+ return true
+ end,
+ enumerate_auths = function()
+ conn:query(enumerate_auths_query)
+ local res = conn:store_result()
+ return function()
+ local row = res:fetch('n')
+ if not row then
+ return nil
+ end
+ local username, password, privs_str, lastlogin = unpack(row)
+ return username, {
+ password = password,
+ privileges = minetest.string_to_privs(privs_str),
+ last_login = tonumber(lastlogin)
+ }
+ end
+ end
+ }
+end
+
+minetest.register_authentication_handler(thismod.auth_handler)
+minetest.log('action', modname .. ": Registered auth handler")
+
+mysql_base.register_on_shutdown(function()
+ thismod.get_auth_stmt:free_result()
+end)
diff --git a/worldmods/mysql_base/LICENSE.txt b/worldmods/mysql_base/LICENSE.txt
new file mode 100644
index 0000000..65c5ca8
--- /dev/null
+++ b/worldmods/mysql_base/LICENSE.txt
@@ -0,0 +1,165 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/worldmods/mysql_base/README.md b/worldmods/mysql_base/README.md
new file mode 100644
index 0000000..56cd50b
--- /dev/null
+++ b/worldmods/mysql_base/README.md
@@ -0,0 +1,108 @@
+# mysql_base
+
+Base Minetest mod to connect to a MySQL database. Used by other mods to read/write data.
+
+# Installing
+
+Get this repository's contents using `git`, and make sure to fetch submodules
+(`git submodule update --init`).
+
+# Configuration
+
+First, if mod security is enabled (`secure.enable_security = true`), this mod must be added as
+a trusted mod (in the `secure.trusted_mods` config entry). There is **no** other solution to
+make it work under mod security.
+
+By default `mysql_base` doesn't run in singleplayer. This can be overriden by setting
+`mysql_base.enable_singleplayer` to `true`.
+
+Configuration may be done as regular Minetest settings entries, or using a config file, allowing
+for more configuration options; to do so specify the path as `mysql_base.cfgfile`. This config
+must contain a Lua table that can be read by `minetest.deserialize`, i.e. a regular table
+definition follwing a `return` statement (see the example below).
+
+When using flat Minetest configuation entries, all the following option names must be prefixed
+with `mysql_base.`. When using a config file, entries are to be hierarchised as per the dot
+separator.
+
+Values written next to option names are default values.
+
+## Database connection
+
+### Minetest flat config file
+
+Values after the "`=`" are the default values used if unspecified.
+```
+mysql_base.db.host = 'localhost'
+mysql_base.db.user = nil -- MySQL connector defaults to current username
+mysql_base.db.pass = nil -- Using password: NO
+mysql_base.db.port = nil -- MySQL connector defaults to either 3306, or no port if using localhost/unix socket
+mysql_base.db.db = nil -- <== Setting this is required
+```
+
+### Lua table config file
+
+Connection options are passed as a table through the `db.connopts` entry.
+Its format must be the same as [LuaPower's MySQL module `mysql.connect(options_t)` function][mycn],
+that is (all members are optional);
+
+```lua
+return {
+ db = {
+ connopts = {
+ host = ...,
+ user = ...,
+ pass = ...,
+ db = ...,
+ port = ...,
+ unix_socket = ...,
+ flags = { ... },
+ options = { ... },
+ attrs = { ... },
+ -- Also key, cert, ca, cpath, cipher
+ }
+ }
+}
+```
+
+## Examples
+
+### Example 1
+
+#### Using a Lua config file
+
+`minetest.conf`:
+```
+mysql_auth.cfgfile = /srv/minetest/skyblock/mysql_auth_config
+```
+
+`/srv/minetest/skyblock/mysql_auth_config`:
+```lua
+return {
+ db = {
+ connopts = {
+ user = 'minetest',
+ pass = 'BQy77wK$Um6es3Bi($iZ*w3N',
+ db = 'minetest'
+ },
+ }
+}
+```
+
+#### Using only Minetest config entries
+
+`minetest.conf`:
+```
+mysql_auth.db.user = minetest
+mysql_auth.db.pass = BQy77wK$Um6es3Bi($iZ*w3N
+mysql_auth.db.db = minetest
+```
+
+# License
+
+`mysql_base` is licensed under [LGPLv3](https://www.gnu.org/licenses/lgpl.html).
+
+Using the Public Domain-licensed LuaPower `mysql` module.
+
+
+[mycn]: https://luapower.com/mysql#mysql.connectoptions_t---conn
diff --git a/worldmods/mysql_base/init.lua b/worldmods/mysql_base/init.lua
new file mode 100644
index 0000000..c0f0591
--- /dev/null
+++ b/worldmods/mysql_base/init.lua
@@ -0,0 +1,186 @@
+local modname = minetest.get_current_modname()
+local modpath = minetest.get_modpath(modname)
+
+local thismod = {
+ enabled = false,
+}
+_G[modname] = thismod
+
+local singleplayer = minetest.is_singleplayer() -- Caching is OK since you can't open a game to
+-- multiplayer unless you restart it.
+if not minetest.setting_get(modname .. '.enable_singleplayer') and singleplayer then
+ minetest.log('action', modname .. ": Not enabling because of singleplayer game")
+ return
+end
+
+thismod.enabled = true
+
+local function setoverlay(tab, orig)
+ local mt = getmetatable(tab) or {}
+ mt.__index = function (tab, key)
+ if rawget(tab, key) ~= nil then
+ return rawget(tab, key)
+ else
+ return orig[key]
+ end
+ end
+ setmetatable(tab, mt)
+end
+
+local function string_splitdots(s)
+ local temp = {}
+ local index = 0
+ local last_index = string.len(s)
+ while true do
+ local i, e = string.find(s, '%.', index)
+ if i and e then
+ local next_index = e + 1
+ local word_bound = i - 1
+ table.insert(temp, string.sub(s, index, word_bound))
+ index = next_index
+ else
+ if index > 0 and index <= last_index then
+ table.insert(temp, string.sub(s, index, last_index))
+ elseif index == 0 then
+ temp = nil
+ end
+ break
+ end
+ end
+ return temp
+end
+
+local mysql
+do -- MySQL module loading
+ local env = {
+ require = function (module)
+ if module == 'mysql_h' then
+ return dofile(modpath .. '/mysql/mysql_h.lua')
+ else
+ return require(module)
+ end
+ end
+ }
+ setoverlay(env, _G)
+ local fn, msg = loadfile(modpath .. '/mysql/mysql.lua')
+ if not fn then error(msg) end
+ setfenv(fn, env)
+ local status
+ status, mysql = pcall(fn, {})
+ if not status then
+ error(modname .. ' failed to load MySQL FFI interface: ' .. tostring(mysql))
+ end
+ thismod.mysql = mysql
+end
+
+function thismod.mkget(modname)
+ local get = function (name) return minetest.setting_get(modname .. '.' .. name) end
+ local cfgfile = get('cfgfile')
+ if type(cfgfile) == 'string' and cfgfile ~= '' then
+ local file = io.open(cfgfile, 'rb')
+ if not file then
+ error(modname .. ' failed to load specified config file at ' .. cfgfile)
+ end
+ local cfg, msg = minetest.deserialize(file:read('*a'))
+ file:close()
+ if not cfg then
+ error(modname .. ' failed to parse specified config file at ' .. cfgfile .. ': ' .. msg)
+ end
+ get = function (name)
+ if type(name) ~= 'string' or name == '' then
+ return nil
+ end
+ local parts = string_splitdots(name)
+ if not parts then
+ return cfg[name]
+ end
+ local tbl = cfg[parts[1]]
+ for n = 2, #parts do
+ if tbl == nil then
+ return nil
+ end
+ tbl = tbl[parts[n]]
+ end
+ return tbl
+ end
+ end
+ return get
+end
+
+local get = thismod.mkget(modname)
+do
+ local conn, dbname
+ -- MySQL API backend
+ mysql.config(get('db.api'))
+
+ local connopts = get('db.connopts')
+ if (get('db.db') == nil) and (type(connopts) == 'table' and connopts.db == nil) then
+ error(modname .. ": missing database name parameter")
+ end
+ if type(connopts) ~= 'table' then
+ connopts = {}
+ -- Traditional connection parameters
+ connopts.host, connopts.user, connopts.port, connopts.pass, connopts.db =
+ get('db.host') or 'localhost', get('db.user'), get('db.port'), get('db.pass'), get('db.db')
+ end
+ connopts.charset = 'utf8'
+ connopts.options = connopts.options or {}
+ connopts.options.MYSQL_OPT_RECONNECT = true
+ conn = mysql.connect(connopts)
+ dbname = connopts.db
+ minetest.log('action', modname .. ": Connected to MySQL database " .. dbname)
+ thismod.conn = conn
+ thismod.dbname = dbname
+
+ -- LuaPower's MySQL interface throws an error when the connection fails, no need to check if
+ -- it succeeded.
+
+ -- Ensure UTF-8 is in use.
+ -- If you use another encoding, kill yourself (unless it's UTF-32).
+ conn:query("SET NAMES 'utf8'")
+ conn:query("SET CHARACTER SET utf8")
+ conn:query("SET character_set_results = 'utf8', character_set_client = 'utf8'," ..
+ "character_set_connection = 'utf8', character_set_database = 'utf8'," ..
+ "character_set_server = 'utf8'")
+
+ local set = function(setting, val) conn:query('SET ' .. setting .. '=' .. val) end
+ pcall(set, 'wait_timeout', 3600)
+ pcall(set, 'autocommit', 1)
+ pcall(set, 'max_allowed_packet', 67108864)
+end
+
+local function ping()
+ if thismod.conn then
+ if not thismod.conn:ping() then
+ minetest.log('error', modname .. ": failed to ping database")
+ end
+ end
+ minetest.after(1800, ping)
+end
+minetest.after(10, ping)
+
+local shutdown_callbacks = {}
+function thismod.register_on_shutdown(func)
+ table.insert(shutdown_callbacks, func)
+end
+
+minetest.register_on_shutdown(function()
+ if thismod.conn then
+ minetest.log('action', modname .. ": Shutting down, running callbacks")
+ for _, func in ipairs(shutdown_callbacks) do
+ func()
+ end
+ thismod.conn:close()
+ thismod.conn = nil
+ minetest.log('action', modname .. ": Cosed database connection")
+ end
+end)
+
+function thismod.table_exists(name)
+ thismod.conn:query("SHOW TABLES LIKE '" .. name .. "'")
+ local res = thismod.conn:store_result()
+ local exists = (res:row_count() ~= 0)
+ res:free()
+ return exists
+end
+
diff --git a/worldmods/mysql_base/mysql/mysql.lua b/worldmods/mysql_base/mysql/mysql.lua
new file mode 100644
index 0000000..9ce07d2
--- /dev/null
+++ b/worldmods/mysql_base/mysql/mysql.lua
@@ -0,0 +1,1303 @@
+
+--mySQL client library ffi binding.
+--Written by Cosmin Apreutesei. Public domain.
+
+--Supports mysql Connector/C 6.1.
+--Based on mySQL 5.7 manual.
+
+if not ... then require'mysql_test' end
+
+local ffi = require'ffi'
+local bit = require'bit'
+require'mysql_h'
+
+local C
+local M = {}
+
+--select a mysql client library implementation.
+local function config(lib)
+ if not C then
+ if not lib or lib == 'mysql' then
+ C = ffi.load(ffi.abi'win' and 'libmysql' or 'mysqlclient')
+ elseif lib == 'mariadb' then
+ C = ffi.load(ffi.abi'win' and 'libmariadb' or 'mariadb')
+ elseif type(lib) == 'string' then
+ C = ffi.load(lib)
+ else
+ C = lib
+ end
+ M.C = C
+ end
+ return M
+end
+
+M.config = config
+
+--we compare NULL pointers against NULL instead of nil for compatibility with luaffi.
+local NULL = ffi.cast('void*', nil)
+
+local function ptr(p) --convert NULLs to nil
+ if p == NULL then return nil end
+ return p
+end
+
+local function cstring(data) --convert null-term non-empty C strings to lua strings
+ if data == NULL or data[0] == 0 then return nil end
+ return ffi.string(data)
+end
+
+--error reporting
+
+local function myerror(mysql, stacklevel)
+ local err = cstring(C.mysql_error(mysql))
+ if not err then return end
+ error(string.format('mysql error: %s', err), stacklevel or 3)
+end
+
+local function checkz(mysql, ret)
+ if ret == 0 then return end
+ myerror(mysql, 4)
+end
+
+local function checkh(mysql, ret)
+ if ret ~= NULL then return ret end
+ myerror(mysql, 4)
+end
+
+local function enum(e, prefix)
+ local v = type(e) == 'string' and (prefix and C[prefix..e] or C[e]) or e
+ return assert(v, 'invalid enum value')
+end
+
+--client library info
+
+function M.thread_safe()
+ config()
+ return C.mysql_thread_safe() == 1
+end
+
+function M.client_info()
+ config()
+ return cstring(C.mysql_get_client_info())
+end
+
+function M.client_version()
+ config()
+ return tonumber(C.mysql_get_client_version())
+end
+
+--connections
+
+local function bool_ptr(b)
+ return ffi.new('my_bool[1]', b or false)
+end
+
+local function uint_bool_ptr(b)
+ return ffi.new('uint32_t[1]', b or false)
+end
+
+local function uint_ptr(i)
+ return ffi.new('uint32_t[1]', i)
+end
+
+local function proto_ptr(proto) --proto is 'MYSQL_PROTOCOL_*' or mysql.C.MYSQL_PROTOCOL_*
+ return ffi.new('uint32_t[1]', enum(proto))
+end
+
+local function ignore_arg()
+ return nil
+end
+
+local option_encoders = {
+ MYSQL_ENABLE_CLEARTEXT_PLUGIN = bool_ptr,
+ MYSQL_OPT_LOCAL_INFILE = uint_bool_ptr,
+ MYSQL_OPT_PROTOCOL = proto_ptr,
+ MYSQL_OPT_READ_TIMEOUT = uint_ptr,
+ MYSQL_OPT_WRITE_TIMEOUT = uint_ptr,
+ MYSQL_OPT_USE_REMOTE_CONNECTION = ignore_arg,
+ MYSQL_OPT_USE_EMBEDDED_CONNECTION = ignore_arg,
+ MYSQL_OPT_GUESS_CONNECTION = ignore_arg,
+ MYSQL_SECURE_AUTH = bool_ptr,
+ MYSQL_REPORT_DATA_TRUNCATION = bool_ptr,
+ MYSQL_OPT_RECONNECT = bool_ptr,
+ MYSQL_OPT_SSL_VERIFY_SERVER_CERT = bool_ptr,
+ MYSQL_ENABLE_CLEARTEXT_PLUGIN = bool_ptr,
+ MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS = bool_ptr,
+}
+
+function M.connect(t, ...)
+ config()
+ local host, user, pass, db, charset, port
+ local unix_socket, flags, options, attrs
+ local key, cert, ca, capath, cipher
+ if type(t) == 'string' then
+ host, user, pass, db, charset, port = t, ...
+ else
+ host, user, pass, db, charset, port = t.host, t.user, t.pass, t.db, t.charset, t.port
+ unix_socket, flags, options, attrs = t.unix_socket, t.flags, t.options, t.attrs
+ key, cert, ca, capath, cipher = t.key, t.cert, t.ca, t.capath, t.cipher
+ end
+ port = port or 0
+
+ local client_flag = 0
+ if type(flags) == 'number' then
+ client_flag = flags
+ elseif flags then
+ for k,v in pairs(flags) do
+ local flag = enum(k, 'MYSQL_') --'CLIENT_*' or mysql.C.MYSQL_CLIENT_* enum
+ client_flag = v and bit.bor(client_flag, flag) or bit.band(client_flag, bit.bnot(flag))
+ end
+ end
+
+ local mysql = assert(C.mysql_init(nil))
+ ffi.gc(mysql, C.mysql_close)
+
+ if options then
+ for k,v in pairs(options) do
+ local opt = enum(k) --'MYSQL_OPT_*' or mysql.C.MYSQL_OPT_* enum
+ local encoder = option_encoders[k]
+ if encoder then v = encoder(v) end
+ assert(C.mysql_options(mysql, opt, ffi.cast('const void*', v)) == 0, 'invalid option')
+ end
+ end
+
+ if attrs then
+ for k,v in pairs(attrs) do
+ assert(C.mysql_options4(mysql, C.MYSQL_OPT_CONNECT_ATTR_ADD, k, v) == 0)
+ end
+ end
+
+ if key then
+ checkz(mysql, C.mysql_ssl_set(mysql, key, cert, ca, capath, cipher))
+ end
+
+ checkh(mysql, C.mysql_real_connect(mysql, host, user, pass, db, port, unix_socket, client_flag))
+
+ if charset then mysql:set_charset(charset) end
+
+ return mysql
+end
+
+local conn = {} --connection methods
+
+function conn.close(mysql)
+ C.mysql_close(mysql)
+ ffi.gc(mysql, nil)
+end
+
+function conn.set_charset(mysql, charset)
+ checkz(mysql, C.mysql_set_character_set(mysql, charset))
+end
+
+function conn.select_db(mysql, db)
+ checkz(mysql, C.mysql_select_db(mysql, db))
+end
+
+function conn.change_user(mysql, user, pass, db)
+ checkz(mysql, C.mysql_change_user(mysql, user, pass, db))
+end
+
+function conn.set_multiple_statements(mysql, yes)
+ checkz(mysql, C.mysql_set_server_option(mysql, yes and C.MYSQL_OPTION_MULTI_STATEMENTS_ON or
+ C.MYSQL_OPTION_MULTI_STATEMENTS_OFF))
+end
+
+--connection info
+
+function conn.charset(mysql)
+ return cstring(C.mysql_character_set_name(mysql))
+end
+
+function conn.charset_info(mysql)
+ local info = ffi.new'MY_CHARSET_INFO'
+ checkz(C.mysql_get_character_set_info(mysql, info))
+ assert(info.name ~= NULL)
+ assert(info.csname ~= NULL)
+ return {
+ number = info.number,
+ state = info.state,
+ name = cstring(info.csname), --csname and name are inverted from the spec
+ collation = cstring(info.name),
+ comment = cstring(info.comment),
+ dir = cstring(info.dir),
+ mbminlen = info.mbminlen,
+ mbmaxlen = info.mbmaxlen,
+ }
+end
+
+function conn.ping(mysql)
+ local ret = C.mysql_ping(mysql)
+ if ret == 0 then
+ return true
+ elseif C.mysql_error(mysql) == C.MYSQL_CR_SERVER_GONE_ERROR then
+ return false
+ end
+ myerror(mysql)
+end
+
+function conn.thread_id(mysql)
+ return C.mysql_thread_id(mysql) --NOTE: result is cdata on x64!
+end
+
+function conn.stat(mysql)
+ return cstring(checkh(mysql, C.mysql_stat(mysql)))
+end
+
+function conn.server_info(mysql)
+ return cstring(checkh(mysql, C.mysql_get_server_info(mysql)))
+end
+
+function conn.host_info(mysql)
+ return cstring(checkh(mysql, C.mysql_get_host_info(mysql)))
+end
+
+function conn.server_version(mysql)
+ return tonumber(C.mysql_get_server_version(mysql))
+end
+
+function conn.proto_info(...)
+ return C.mysql_get_proto_info(...)
+end
+
+function conn.ssl_cipher(mysql)
+ return cstring(C.mysql_get_ssl_cipher(mysql))
+end
+
+--transactions
+
+function conn.commit(mysql) checkz(mysql, C.mysql_commit(mysql)) end
+function conn.rollback(mysql) checkz(mysql, C.mysql_rollback(mysql)) end
+function conn.set_autocommit(mysql, yes)
+ checkz(mysql, C.mysql_autocommit(mysql, yes == nil or yes))
+end
+
+--queries
+
+function conn.escape_tobuffer(mysql, data, size, buf, sz)
+ size = size or #data
+ assert(sz >= size * 2 + 1)
+ return tonumber(C.mysql_real_escape_string(mysql, buf, data, size))
+end
+
+function conn.escape(mysql, data, size)
+ size = size or #data
+ local sz = size * 2 + 1
+ local buf = ffi.new('uint8_t[?]', sz)
+ sz = conn.escape_tobuffer(mysql, data, size, buf, sz)
+ return ffi.string(buf, sz)
+end
+
+function conn.query(mysql, data, size)
+ checkz(mysql, C.mysql_real_query(mysql, data, size or #data))
+end
+
+--query info
+
+function conn.field_count(...)
+ return C.mysql_field_count(...)
+end
+
+local minus1_uint64 = ffi.cast('uint64_t', ffi.cast('int64_t', -1))
+function conn.affected_rows(mysql)
+ local n = C.mysql_affected_rows(mysql)
+ if n == minus1_uint64 then myerror(mysql) end
+ return tonumber(n)
+end
+
+function conn.insert_id(...)
+ return C.mysql_insert_id(...) --NOTE: result is cdata on x64!
+end
+
+function conn.errno(conn)
+ local err = C.mysql_errno(conn)
+ if err == 0 then return end
+ return err
+end
+
+function conn.sqlstate(mysql)
+ return cstring(C.mysql_sqlstate(mysql))
+end
+
+function conn.warning_count(...)
+ return C.mysql_warning_count(...)
+end
+
+function conn.info(mysql)
+ return cstring(C.mysql_info(mysql))
+end
+
+--query results
+
+function conn.next_result(mysql) --multiple statement queries return multiple results
+ local ret = C.mysql_next_result(mysql)
+ if ret == 0 then return true end
+ if ret == -1 then return false end
+ myerror(mysql)
+end
+
+function conn.more_results(mysql)
+ return C.mysql_more_results(mysql) == 1
+end
+
+local function result_function(func)
+ return function(mysql)
+ local res = checkh(mysql, C[func](mysql))
+ return ffi.gc(res, C.mysql_free_result)
+ end
+end
+
+conn.store_result = result_function'mysql_store_result'
+conn.use_result = result_function'mysql_use_result'
+
+local res = {} --result methods
+
+function res.free(res)
+ C.mysql_free_result(res)
+ ffi.gc(res, nil)
+end
+
+function res.row_count(res)
+ return tonumber(C.mysql_num_rows(res))
+end
+
+function res.field_count(...)
+ return C.mysql_num_fields(...)
+end
+
+function res.eof(res)
+ return C.mysql_eof(res) ~= 0
+end
+
+--field info
+
+local field_type_names = {
+ [ffi.C.MYSQL_TYPE_DECIMAL] = 'decimal', --DECIMAL or NUMERIC
+ [ffi.C.MYSQL_TYPE_TINY] = 'tinyint',
+ [ffi.C.MYSQL_TYPE_SHORT] = 'smallint',
+ [ffi.C.MYSQL_TYPE_LONG] = 'int',
+ [ffi.C.MYSQL_TYPE_FLOAT] = 'float',
+ [ffi.C.MYSQL_TYPE_DOUBLE] = 'double', --DOUBLE or REAL
+ [ffi.C.MYSQL_TYPE_NULL] = 'null',
+ [ffi.C.MYSQL_TYPE_TIMESTAMP] = 'timestamp',
+ [ffi.C.MYSQL_TYPE_LONGLONG] = 'bigint',
+ [ffi.C.MYSQL_TYPE_INT24] = 'mediumint',
+ [ffi.C.MYSQL_TYPE_DATE] = 'date', --pre mysql 5.0, storage = 4 bytes
+ [ffi.C.MYSQL_TYPE_TIME] = 'time',
+ [ffi.C.MYSQL_TYPE_DATETIME] = 'datetime',
+ [ffi.C.MYSQL_TYPE_YEAR] = 'year',
+ [ffi.C.MYSQL_TYPE_NEWDATE] = 'date', --mysql 5.0+, storage = 3 bytes
+ [ffi.C.MYSQL_TYPE_VARCHAR] = 'varchar',
+ [ffi.C.MYSQL_TYPE_BIT] = 'bit',
+ [ffi.C.MYSQL_TYPE_TIMESTAMP2] = 'timestamp', --mysql 5.6+, can store fractional seconds
+ [ffi.C.MYSQL_TYPE_DATETIME2] = 'datetime', --mysql 5.6+, can store fractional seconds
+ [ffi.C.MYSQL_TYPE_TIME2] = 'time', --mysql 5.6+, can store fractional seconds
+ [ffi.C.MYSQL_TYPE_NEWDECIMAL] = 'decimal', --mysql 5.0+, Precision math DECIMAL or NUMERIC
+ [ffi.C.MYSQL_TYPE_ENUM] = 'enum',
+ [ffi.C.MYSQL_TYPE_SET] = 'set',
+ [ffi.C.MYSQL_TYPE_TINY_BLOB] = 'tinyblob',
+ [ffi.C.MYSQL_TYPE_MEDIUM_BLOB] = 'mediumblob',
+ [ffi.C.MYSQL_TYPE_LONG_BLOB] = 'longblob',
+ [ffi.C.MYSQL_TYPE_BLOB] = 'text', --TEXT or BLOB
+ [ffi.C.MYSQL_TYPE_VAR_STRING] = 'varchar', --VARCHAR or VARBINARY
+ [ffi.C.MYSQL_TYPE_STRING] = 'char', --CHAR or BINARY
+ [ffi.C.MYSQL_TYPE_GEOMETRY] = 'spatial', --Spatial field
+}
+
+local binary_field_type_names = {
+ [ffi.C.MYSQL_TYPE_BLOB] = 'blob',
+ [ffi.C.MYSQL_TYPE_VAR_STRING] = 'varbinary',
+ [ffi.C.MYSQL_TYPE_STRING] = 'binary',
+}
+
+local field_flag_names = {
+ [ffi.C.MYSQL_NOT_NULL_FLAG] = 'not_null',
+ [ffi.C.MYSQL_PRI_KEY_FLAG] = 'pri_key',
+ [ffi.C.MYSQL_UNIQUE_KEY_FLAG] = 'unique_key',
+ [ffi.C.MYSQL_MULTIPLE_KEY_FLAG] = 'key',
+ [ffi.C.MYSQL_BLOB_FLAG] = 'is_blob',
+ [ffi.C.MYSQL_UNSIGNED_FLAG] = 'unsigned',
+ [ffi.C.MYSQL_ZEROFILL_FLAG] = 'zerofill',
+ [ffi.C.MYSQL_BINARY_FLAG] = 'is_binary',
+ [ffi.C.MYSQL_ENUM_FLAG] = 'is_enum',
+ [ffi.C.MYSQL_AUTO_INCREMENT_FLAG] = 'autoincrement',
+ [ffi.C.MYSQL_TIMESTAMP_FLAG] = 'is_timestamp',
+ [ffi.C.MYSQL_SET_FLAG] = 'is_set',
+ [ffi.C.MYSQL_NO_DEFAULT_VALUE_FLAG] = 'no_default',
+ [ffi.C.MYSQL_ON_UPDATE_NOW_FLAG] = 'on_update_now',
+ [ffi.C.MYSQL_NUM_FLAG] = 'is_number',
+}
+
+local function field_type_name(info)
+ local type_flag = tonumber(info.type)
+ local field_type = field_type_names[type_flag]
+ --charsetnr 63 changes CHAR into BINARY, VARCHAR into VARBYNARY, TEXT into BLOB
+ field_type = info.charsetnr == 63 and binary_field_type_names[type_flag] or field_type
+ return field_type
+end
+
+--convenience field type fetcher (less garbage)
+function res.field_type(res, i)
+ assert(i >= 1 and i <= res:field_count(), 'index out of range')
+ local info = C.mysql_fetch_field_direct(res, i-1)
+ local unsigned = bit.bor(info.flags, C.MYSQL_UNSIGNED_FLAG) ~= 0
+ return field_type_name(info), tonumber(info.length), unsigned, info.decimals
+end
+
+function res.field_info(res, i)
+ assert(i >= 1 and i <= res:field_count(), 'index out of range')
+ local info = C.mysql_fetch_field_direct(res, i-1)
+ local t = {
+ name = cstring(info.name, info.name_length),
+ org_name = cstring(info.org_name, info.org_name_length),
+ table = cstring(info.table, info.table_length),
+ org_table = cstring(info.org_table, info.org_table_length),
+ db = cstring(info.db, info.db_length),
+ catalog = cstring(info.catalog, info.catalog_length),
+ def = cstring(info.def, info.def_length),
+ length = tonumber(info.length),
+ max_length = tonumber(info.max_length),
+ decimals = info.decimals,
+ charsetnr = info.charsetnr,
+ type_flag = tonumber(info.type),
+ type = field_type_name(info),
+ flags = info.flags,
+ extension = ptr(info.extension),
+ }
+ for flag, name in pairs(field_flag_names) do
+ t[name] = bit.band(flag, info.flags) ~= 0
+ end
+ return t
+end
+
+--convenience field name fetcher (less garbage)
+function res.field_name(res, i)
+ assert(i >= 1 and i <= res:field_count(), 'index out of range')
+ local info = C.mysql_fetch_field_direct(res, i-1)
+ return cstring(info.name, info.name_length)
+end
+
+--convenience field iterator, shortcut for: for i=1,res:field_count() do local field = res:field_info(i) ... end
+function res.fields(res)
+ local n = res:field_count()
+ local i = 0
+ return function()
+ if i == n then return end
+ i = i + 1
+ return i, res:field_info(i)
+ end
+end
+
+--row data fetching and parsing
+
+ffi.cdef('double strtod(const char*, char**);')
+local function parse_int(data, sz) --using strtod to avoid string creation
+ return ffi.C.strtod(data, nil)
+end
+
+local function parse_float(data, sz)
+ return tonumber(ffi.cast('float', ffi.C.strtod(data, nil))) --because windows is missing strtof()
+end
+
+local function parse_double(data, sz)
+ return ffi.C.strtod(data, nil)
+end
+
+ffi.cdef('int64_t strtoll(const char*, char**, int) ' ..(ffi.os == 'Windows' and ' asm("_strtoi64")' or '') .. ';')
+local function parse_int64(data, sz)
+ return ffi.C.strtoll(data, nil, 10)
+end
+
+ffi.cdef('uint64_t strtoull(const char*, char**, int) ' ..(ffi.os == 'Windows' and ' asm("_strtoui64")' or '') .. ';')
+local function parse_uint64(data, sz)
+ return ffi.C.strtoull(data, nil, 10)
+end
+
+local function parse_bit(data, sz)
+ data = ffi.cast('uint8_t*', data) --force unsigned
+ local n = data[0] --this is the msb: bit fields always come in big endian byte order
+ if sz > 6 then --we can cover up to 6 bytes with only Lua numbers
+ n = ffi.new('uint64_t', n)
+ end
+ for i=1,sz-1 do
+ n = n * 256 + data[i]
+ end
+ return n
+end
+
+local function parse_date_(data, sz)
+ assert(sz >= 10)
+ local z = ('0'):byte()
+ local year = (data[0] - z) * 1000 + (data[1] - z) * 100 + (data[2] - z) * 10 + (data[3] - z)
+ local month = (data[5] - z) * 10 + (data[6] - z)
+ local day = (data[8] - z) * 10 + (data[9] - z)
+ return year, month, day
+end
+
+local function parse_time_(data, sz)
+ assert(sz >= 8)
+ local z = ('0'):byte()
+ local hour = (data[0] - z) * 10 + (data[1] - z)
+ local min = (data[3] - z) * 10 + (data[4] - z)
+ local sec = (data[6] - z) * 10 + (data[7] - z)
+ local frac = 0
+ for i = 9, sz-1 do
+ frac = frac * 10 + (data[i] - z)
+ end
+ return hour, min, sec, frac
+end
+
+local function format_date(year, month, day)
+ return string.format('%04d-%02d-%02d', year, month, day)
+end
+
+local function format_time(hour, min, sec, frac)
+ if frac and frac ~= 0 then
+ return string.format('%02d:%02d:%02d.%d', hour, min, sec, frac)
+ else
+ return string.format('%02d:%02d:%02d', hour, min, sec)
+ end
+end
+
+local function datetime_tostring(t)
+ local date, time
+ if t.year then
+ date = format_date(t.year, t.month, t.day)
+ end
+ if t.sec then
+ time = format_time(t.hour, t.min, t.sec, t.frac)
+ end
+ if date and time then
+ return date .. ' ' .. time
+ else
+ return assert(date or time)
+ end
+end
+
+local datetime_meta = {__tostring = datetime_tostring}
+local function datetime(t)
+ return setmetatable(t, datetime_meta)
+end
+
+local function parse_date(data, sz)
+ local year, month, day = parse_date_(data, sz)
+ return datetime{year = year, month = month, day = day}
+end
+
+local function parse_time(data, sz)
+ local hour, min, sec, frac = parse_time_(data, sz)
+ return datetime{hour = hour, min = min, sec = sec, frac = frac}
+end
+
+local function parse_datetime(data, sz)
+ local year, month, day = parse_date_(data, sz)
+ local hour, min, sec, frac = parse_time_(data + 11, sz - 11)
+ return datetime{year = year, month = month, day = day, hour = hour, min = min, sec = sec, frac = frac}
+end
+
+local field_decoders = { --other field types not present here are returned as strings, unparsed
+ [ffi.C.MYSQL_TYPE_TINY] = parse_int,
+ [ffi.C.MYSQL_TYPE_SHORT] = parse_int,
+ [ffi.C.MYSQL_TYPE_LONG] = parse_int,
+ [ffi.C.MYSQL_TYPE_FLOAT] = parse_float,
+ [ffi.C.MYSQL_TYPE_DOUBLE] = parse_double,
+ [ffi.C.MYSQL_TYPE_TIMESTAMP] = parse_datetime,
+ [ffi.C.MYSQL_TYPE_LONGLONG] = parse_int64,
+ [ffi.C.MYSQL_TYPE_INT24] = parse_int,
+ [ffi.C.MYSQL_TYPE_DATE] = parse_date,
+ [ffi.C.MYSQL_TYPE_TIME] = parse_time,
+ [ffi.C.MYSQL_TYPE_DATETIME] = parse_datetime,
+ [ffi.C.MYSQL_TYPE_NEWDATE] = parse_date,
+ [ffi.C.MYSQL_TYPE_TIMESTAMP2] = parse_datetime,
+ [ffi.C.MYSQL_TYPE_DATETIME2] = parse_datetime,
+ [ffi.C.MYSQL_TYPE_TIME2] = parse_time,
+ [ffi.C.MYSQL_TYPE_YEAR] = parse_int,
+ [ffi.C.MYSQL_TYPE_BIT] = parse_bit,
+}
+
+local unsigned_decoders = {
+ [ffi.C.MYSQL_TYPE_LONGLONG] = parse_uint64,
+}
+
+local function mode_flags(mode)
+ local assoc = mode and mode:find'a'
+ local numeric = not mode or not assoc or mode:find'n'
+ local decode = not mode or not mode:find's'
+ local packed = mode and mode:find'[an]'
+ local fetch_fields = assoc or decode --if assoc we need field_name, if decode we need field_type
+ return numeric, assoc, decode, packed, fetch_fields
+end
+
+local function fetch_row(res, numeric, assoc, decode, field_count, fields, t)
+ local values = C.mysql_fetch_row(res)
+ if values == NULL then
+ if res.conn ~= NULL then --buffered read: check for errors
+ myerror(res.conn, 4)
+ end
+ return nil
+ end
+ local sizes = C.mysql_fetch_lengths(res)
+ for i=0,field_count-1 do
+ local v = values[i]
+ if v ~= NULL then
+ local decoder
+ if decode then
+ local ftype = tonumber(fields[i].type)
+ local unsigned = bit.bor(fields[i].flags, C.MYSQL_UNSIGNED_FLAG) ~= 0
+ decoder = unsigned and unsigned_decoders[ftype] or field_decoders[ftype] or ffi.string
+ else
+ decoder = ffi.string
+ end
+ v = decoder(values[i], tonumber(sizes[i]))
+ if numeric then
+ t[i+1] = v
+ end
+ if assoc then
+ local k = ffi.string(fields[i].name, fields[i].name_length)
+ t[k] = v
+ end
+ end
+ end
+ return t
+end
+
+function res.fetch(res, mode, t)
+ local numeric, assoc, decode, packed, fetch_fields = mode_flags(mode)
+ local field_count = C.mysql_num_fields(res)
+ local fields = fetch_fields and C.mysql_fetch_fields(res)
+ local row = fetch_row(res, numeric, assoc, decode, field_count, fields, t or {})
+ if packed then
+ return row
+ else
+ return true, unpack(row)
+ end
+end
+
+function res.rows(res, mode, t)
+ local numeric, assoc, decode, packed, fetch_fields = mode_flags(mode)
+ local field_count = C.mysql_num_fields(res)
+ local fields = fetch_fields and C.mysql_fetch_fields(res)
+ local i = 0
+ res:seek(1)
+ return function()
+ local row = fetch_row(res, numeric, assoc, decode, field_count, fields, t or {})
+ if not row then return end
+ i = i + 1
+ if packed then
+ return i, row
+ else
+ return i, unpack(row)
+ end
+ end
+end
+
+function res.tell(...)
+ return C.mysql_row_tell(...)
+end
+
+function res.seek(res, where) --use in conjunction with res:row_count()
+ if type(where) == 'number' then
+ C.mysql_data_seek(res, where-1)
+ else
+ C.mysql_row_seek(res, where)
+ end
+end
+
+--reflection
+
+local function list_function(func)
+ return function(mysql, wild)
+ local res = checkh(mysql, C[func](mysql, wild))
+ return ffi.gc(res, C.mysql_free_result)
+ end
+end
+
+conn.list_dbs = list_function'mysql_list_dbs'
+conn.list_tables = list_function'mysql_list_tables'
+conn.list_processes = result_function'mysql_list_processes'
+
+--remote control
+
+function conn.kill(mysql, pid)
+ checkz(mysql, C.mysql_kill(mysql, pid))
+end
+
+function conn.shutdown(mysql, level)
+ checkz(mysql, C.mysql_shutdown(mysql, enum(level or C.MYSQL_SHUTDOWN_DEFAULT, 'MYSQL_')))
+end
+
+function conn.refresh(mysql, t) --options are 'REFRESH_*' or mysql.C.MYSQL_REFRESH_* enums
+ local options = 0
+ if type(t) == 'number' then
+ options = t
+ else
+ for k,v in pairs(t) do
+ if v then
+ options = bit.bor(options, enum(k, 'MYSQL_'))
+ end
+ end
+ end
+ checkz(mysql, C.mysql_refresh(mysql, options))
+end
+
+function conn.dump_debug_info(mysql)
+ checkz(mysql, C.mysql_dump_debug_info(mysql))
+end
+
+--prepared statements
+
+local function sterror(stmt, stacklevel)
+ local err = cstring(C.mysql_stmt_error(stmt))
+ if not err then return end
+ error(string.format('mysql error: %s', err), stacklevel or 3)
+end
+
+local function stcheckz(stmt, ret)
+ if ret == 0 then return end
+ sterror(stmt, 4)
+end
+
+local function stcheckbool(stmt, ret)
+ if ret == 1 then return end
+ sterror(stmt, 4)
+end
+
+local function stcheckh(stmt, ret)
+ if ret ~= NULL then return ret end
+ sterror(stmt, 4)
+end
+
+function conn.prepare(mysql, query)
+ local stmt = checkh(mysql, C.mysql_stmt_init(mysql))
+ ffi.gc(stmt, C.mysql_stmt_close)
+ stcheckz(stmt, C.mysql_stmt_prepare(stmt, query, #query))
+ return stmt
+end
+
+local stmt = {} --statement methods
+
+function stmt.close(stmt)
+ stcheckbool(stmt, C.mysql_stmt_close(stmt))
+ ffi.gc(stmt, nil)
+end
+
+function stmt.exec(stmt)
+ stcheckz(stmt, C.mysql_stmt_execute(stmt))
+end
+
+function stmt.next_result(stmt)
+ local ret = C.mysql_stmt_next_result(stmt)
+ if ret == 0 then return true end
+ if ret == -1 then return false end
+ sterror(stmt)
+end
+
+function stmt.store_result(stmt)
+ stcheckz(stmt, C.mysql_stmt_store_result(stmt))
+end
+
+function stmt.free_result(stmt)
+ stcheckbool(stmt, C.mysql_stmt_free_result(stmt))
+end
+
+function stmt.row_count(stmt)
+ return tonumber(C.mysql_stmt_num_rows(stmt))
+end
+
+function stmt.affected_rows(stmt)
+ local n = C.mysql_stmt_affected_rows(stmt)
+ if n == minus1_uint64 then sterror(stmt) end
+ return tonumber(n)
+end
+
+function stmt.insert_id(...)
+ return C.mysql_stmt_insert_id(...)
+end
+
+function stmt.field_count(stmt)
+ return tonumber(C.mysql_stmt_field_count(stmt))
+end
+
+function stmt.param_count(stmt)
+ return tonumber(C.mysql_stmt_param_count(stmt))
+end
+
+function stmt.errno(stmt)
+ local err = C.mysql_stmt_errno(stmt)
+ if err == 0 then return end
+ return err
+end
+
+function stmt.sqlstate(stmt)
+ return cstring(C.mysql_stmt_sqlstate(stmt))
+end
+
+function stmt.result_metadata(stmt)
+ local res = stcheckh(stmt, C.mysql_stmt_result_metadata(stmt))
+ return res and ffi.gc(res, C.mysql_free_result)
+end
+
+function stmt.fields(stmt)
+ local res = stmt:result_metadata()
+ if not res then return nil end
+ local fields = res:fields()
+ return function()
+ local i, info = fields()
+ if not i then
+ res:free()
+ end
+ return i, info
+ end
+end
+
+function stmt.fetch(stmt)
+ local ret = C.mysql_stmt_fetch(stmt)
+ if ret == 0 then return true end
+ if ret == C.MYSQL_NO_DATA then return false end
+ if ret == C.MYSQL_DATA_TRUNCATED then return true, 'truncated' end
+ sterror(stmt)
+end
+
+function stmt.reset(stmt)
+ stcheckz(stmt, C.mysql_stmt_reset(stmt))
+end
+
+function stmt.tell(...)
+ return C.mysql_stmt_row_tell(...)
+end
+
+function stmt.seek(stmt, where) --use in conjunction with stmt:row_count()
+ if type(where) == 'number' then
+ C.mysql_stmt_data_seek(stmt, where-1)
+ else
+ C.mysql_stmt_row_seek(stmt, where)
+ end
+end
+
+function stmt.write(stmt, param_number, data, size)
+ stcheckz(stmt, C.mysql_stmt_send_long_data(stmt, param_number, data, size or #data))
+end
+
+function stmt.update_max_length(stmt)
+ local attr = ffi.new'my_bool[1]'
+ stcheckz(stmt, C.mysql_stmt_attr_get(stmt, C.STMT_ATTR_UPDATE_MAX_LENGTH, attr))
+ return attr[0] == 1
+end
+
+function stmt.set_update_max_length(stmt, yes)
+ local attr = ffi.new('my_bool[1]', yes == nil or yes)
+ stcheckz(stmt, C.mysql_stmt_attr_set(stmt, C.STMT_ATTR_CURSOR_TYPE, attr))
+end
+
+function stmt.cursor_type(stmt)
+ local attr = ffi.new'uint32_t[1]'
+ stcheckz(stmt, C.mysql_stmt_attr_get(stmt, C.STMT_ATTR_CURSOR_TYPE, attr))
+ return attr[0]
+end
+
+function stmt.set_cursor_type(stmt, cursor_type)
+ local attr = ffi.new('uint32_t[1]', enum(cursor_type, 'MYSQL_'))
+ stcheckz(stmt, C.mysql_stmt_attr_set(stmt, C.STMT_ATTR_CURSOR_TYPE, attr))
+end
+
+function stmt.prefetch_rows(stmt)
+ local attr = ffi.new'uint32_t[1]'
+ stcheckz(stmt, C.mysql_stmt_attr_get(stmt, C.STMT_ATTR_PREFETCH_ROWS, attr))
+ return attr[0]
+end
+
+function stmt.set_prefetch_rows(stmt, n)
+ local attr = ffi.new('uint32_t[1]', n)
+ stcheckz(stmt, C.mysql_stmt_attr_set(stmt, C.STMT_ATTR_PREFETCH_ROWS, attr))
+end
+
+--prepared statements / bind buffers
+
+--see http://dev.mysql.com/doc/refman/5.7/en/c-api-prepared-statement-type-codes.html
+local bb_types_input = {
+ --conversion-free types
+ tinyint = ffi.C.MYSQL_TYPE_TINY,
+ smallint = ffi.C.MYSQL_TYPE_SHORT,
+ int = ffi.C.MYSQL_TYPE_LONG,
+ integer = ffi.C.MYSQL_TYPE_LONG, --alias of int
+ bigint = ffi.C.MYSQL_TYPE_LONGLONG,
+ float = ffi.C.MYSQL_TYPE_FLOAT,
+ double = ffi.C.MYSQL_TYPE_DOUBLE,
+ time = ffi.C.MYSQL_TYPE_TIME,
+ date = ffi.C.MYSQL_TYPE_DATE,
+ datetime = ffi.C.MYSQL_TYPE_DATETIME,
+ timestamp = ffi.C.MYSQL_TYPE_TIMESTAMP,
+ text = ffi.C.MYSQL_TYPE_STRING,
+ char = ffi.C.MYSQL_TYPE_STRING,
+ varchar = ffi.C.MYSQL_TYPE_STRING,
+ blob = ffi.C.MYSQL_TYPE_BLOB,
+ binary = ffi.C.MYSQL_TYPE_BLOB,
+ varbinary = ffi.C.MYSQL_TYPE_BLOB,
+ null = ffi.C.MYSQL_TYPE_NULL,
+ --conversion types (can only use one of the above C types)
+ mediumint = ffi.C.MYSQL_TYPE_LONG,
+ real = ffi.C.MYSQL_TYPE_DOUBLE,
+ decimal = ffi.C.MYSQL_TYPE_BLOB,
+ numeric = ffi.C.MYSQL_TYPE_BLOB,
+ year = ffi.C.MYSQL_TYPE_SHORT,
+ tinyblob = ffi.C.MYSQL_TYPE_BLOB,
+ tinytext = ffi.C.MYSQL_TYPE_BLOB,
+ mediumblob = ffi.C.MYSQL_TYPE_BLOB,
+ mediumtext = ffi.C.MYSQL_TYPE_BLOB,
+ longblob = ffi.C.MYSQL_TYPE_BLOB,
+ longtext = ffi.C.MYSQL_TYPE_BLOB,
+ bit = ffi.C.MYSQL_TYPE_LONGLONG, --MYSQL_TYPE_BIT is not available for input params
+ set = ffi.C.MYSQL_TYPE_BLOB,
+ enum = ffi.C.MYSQL_TYPE_BLOB,
+}
+
+local bb_types_output = {
+ --conversion-free types
+ tinyint = ffi.C.MYSQL_TYPE_TINY,
+ smallint = ffi.C.MYSQL_TYPE_SHORT,
+ mediumint = ffi.C.MYSQL_TYPE_INT24, --int32
+ int = ffi.C.MYSQL_TYPE_LONG,
+ integer = ffi.C.MYSQL_TYPE_LONG, --alias of int
+ bigint = ffi.C.MYSQL_TYPE_LONGLONG,
+ float = ffi.C.MYSQL_TYPE_FLOAT,
+ double = ffi.C.MYSQL_TYPE_DOUBLE,
+ real = ffi.C.MYSQL_TYPE_DOUBLE,
+ decimal = ffi.C.MYSQL_TYPE_NEWDECIMAL, --char[]
+ numeric = ffi.C.MYSQL_TYPE_NEWDECIMAL, --char[]
+ year = ffi.C.MYSQL_TYPE_SHORT,
+ time = ffi.C.MYSQL_TYPE_TIME,
+ date = ffi.C.MYSQL_TYPE_DATE,
+ datetime = ffi.C.MYSQL_TYPE_DATETIME,
+ timestamp = ffi.C.MYSQL_TYPE_TIMESTAMP,
+ char = ffi.C.MYSQL_TYPE_STRING,
+ binary = ffi.C.MYSQL_TYPE_STRING,
+ varchar = ffi.C.MYSQL_TYPE_VAR_STRING,
+ varbinary = ffi.C.MYSQL_TYPE_VAR_STRING,
+ tinyblob = ffi.C.MYSQL_TYPE_TINY_BLOB,
+ tinytext = ffi.C.MYSQL_TYPE_TINY_BLOB,
+ blob = ffi.C.MYSQL_TYPE_BLOB,
+ text = ffi.C.MYSQL_TYPE_BLOB,
+ mediumblob = ffi.C.MYSQL_TYPE_MEDIUM_BLOB,
+ mediumtext = ffi.C.MYSQL_TYPE_MEDIUM_BLOB,
+ longblob = ffi.C.MYSQL_TYPE_LONG_BLOB,
+ longtext = ffi.C.MYSQL_TYPE_LONG_BLOB,
+ bit = ffi.C.MYSQL_TYPE_BIT,
+ --conversion types (can only use one of the above C types)
+ null = ffi.C.MYSQL_TYPE_TINY,
+ set = ffi.C.MYSQL_TYPE_BLOB,
+ enum = ffi.C.MYSQL_TYPE_BLOB,
+}
+
+local number_types = {
+ [ffi.C.MYSQL_TYPE_TINY] = 'int8_t[1]',
+ [ffi.C.MYSQL_TYPE_SHORT] = 'int16_t[1]',
+ [ffi.C.MYSQL_TYPE_LONG] = 'int32_t[1]',
+ [ffi.C.MYSQL_TYPE_INT24] = 'int32_t[1]',
+ [ffi.C.MYSQL_TYPE_LONGLONG] = 'int64_t[1]',
+ [ffi.C.MYSQL_TYPE_FLOAT] = 'float[1]',
+ [ffi.C.MYSQL_TYPE_DOUBLE] = 'double[1]',
+}
+
+local uint_types = {
+ [ffi.C.MYSQL_TYPE_TINY] = 'uint8_t[1]',
+ [ffi.C.MYSQL_TYPE_SHORT] = 'uint16_t[1]',
+ [ffi.C.MYSQL_TYPE_LONG] = 'uint32_t[1]',
+ [ffi.C.MYSQL_TYPE_INT24] = 'uint32_t[1]',
+ [ffi.C.MYSQL_TYPE_LONGLONG] = 'uint64_t[1]',
+}
+
+local time_types = {
+ [ffi.C.MYSQL_TYPE_TIME] = true,
+ [ffi.C.MYSQL_TYPE_DATE] = true,
+ [ffi.C.MYSQL_TYPE_DATETIME] = true,
+ [ffi.C.MYSQL_TYPE_TIMESTAMP] = true,
+}
+
+local time_struct_types = {
+ [ffi.C.MYSQL_TYPE_TIME] = ffi.C.MYSQL_TIMESTAMP_TIME,
+ [ffi.C.MYSQL_TYPE_DATE] = ffi.C.MYSQL_TIMESTAMP_DATE,
+ [ffi.C.MYSQL_TYPE_DATETIME] = ffi.C.MYSQL_TIMESTAMP_DATETIME,
+ [ffi.C.MYSQL_TYPE_TIMESTAMP] = ffi.C.MYSQL_TIMESTAMP_DATETIME,
+}
+
+local params = {} --params bind buffer methods
+local params_meta = {__index = params}
+local fields = {} --params bind buffer methods
+local fields_meta = {__index = fields}
+
+-- "varchar(200)" -> "varchar", 200; "decimal(10,4)" -> "decimal", 12; "int unsigned" -> "int", nil, true
+local function parse_type(s)
+ s = s:lower()
+ local unsigned = false
+ local rest = s:match'(.-)%s+unsigned$'
+ if rest then s, unsigned = rest, true end
+ local rest, sz = s:match'^%s*([^%(]+)%s*%(%s*(%d+)[^%)]*%)%s*$'
+ if rest then
+ s, sz = rest, assert(tonumber(sz), 'invalid type')
+ if s == 'decimal' or s == 'numeric' then --make room for the dot and the minus sign
+ sz = sz + 2
+ end
+ end
+ return s, sz, unsigned
+end
+
+local function bind_buffer(bb_types, meta, types)
+ local self = setmetatable({}, meta)
+
+ self.count = #types
+ self.buffer = ffi.new('MYSQL_BIND[?]', #types)
+ self.data = {} --data buffers, one for each field
+ self.lengths = ffi.new('unsigned long[?]', #types) --length buffers, one for each field
+ self.null_flags = ffi.new('my_bool[?]', #types) --null flag buffers, one for each field
+ self.error_flags = ffi.new('my_bool[?]', #types) --error (truncation) flag buffers, one for each field
+
+ for i,typedef in ipairs(types) do
+ local stype, size, unsigned = parse_type(typedef)
+ local btype = assert(bb_types[stype], 'invalid type')
+ local data
+ if stype == 'bit' then
+ if btype == C.MYSQL_TYPE_LONGLONG then --for input: use unsigned int64 and ignore size
+ data = ffi.new'uint64_t[1]'
+ self.buffer[i-1].is_unsigned = 1
+ size = 0
+ elseif btype == C.MYSQL_TYPE_BIT then --for output: use mysql conversion-free type
+ size = size or 64 --if missing size, assume maximum
+ size = math.ceil(size / 8)
+ assert(size >= 1 and size <= 8, 'invalid size')
+ data = ffi.new('uint8_t[?]', size)
+ end
+ elseif number_types[btype] then
+ assert(not size, 'fixed size type')
+ data = ffi.new(unsigned and uint_types[btype] or number_types[btype])
+ self.buffer[i-1].is_unsigned = unsigned
+ size = ffi.sizeof(data)
+ elseif time_types[btype] then
+ assert(not size, 'fixed size type')
+ data = ffi.new'MYSQL_TIME'
+ data.time_type = time_struct_types[btype]
+ size = 0
+ elseif btype == C.MYSQL_TYPE_NULL then
+ assert(not size, 'fixed size type')
+ size = 0
+ else
+ assert(size, 'missing size')
+ data = size > 0 and ffi.new('uint8_t[?]', size) or nil
+ end
+ self.null_flags[i-1] = true
+ self.data[i] = data
+ self.lengths[i-1] = 0
+ self.buffer[i-1].buffer_type = btype
+ self.buffer[i-1].buffer = data
+ self.buffer[i-1].buffer_length = size
+ self.buffer[i-1].is_null = self.null_flags + (i - 1)
+ self.buffer[i-1].error = self.error_flags + (i - 1)
+ self.buffer[i-1].length = self.lengths + (i - 1)
+ end
+ return self
+end
+
+local function params_bind_buffer(types)
+ return bind_buffer(bb_types_input, params_meta, types)
+end
+
+local function fields_bind_buffer(types)
+ return bind_buffer(bb_types_output, fields_meta, types)
+end
+
+local function bind_check_range(self, i)
+ assert(i >= 1 and i <= self.count, 'index out of bounds')
+end
+
+--realloc a buffer using supplied size. only for varsize fields.
+function params:realloc(i, size)
+ bind_check_range(self, i)
+ assert(ffi.istype(data, 'uint8_t[?]'), 'attempt to realloc a fixed size field')
+ local data = size > 0 and ffi.new('uint8_t[?]', size) or nil
+ self.null_flags[i-1] = true
+ self.data[i] = data
+ self.lengths[i-1] = 0
+ self.buffer[i-1].buffer = data
+ self.buffer[i-1].buffer_length = size
+end
+
+fields.realloc = params.realloc
+
+function fields:get_date(i)
+ bind_check_range(self, i)
+ local btype = tonumber(self.buffer[i-1].buffer_type)
+ local date = btype == C.MYSQL_TYPE_DATE or btype == C.MYSQL_TYPE_DATETIME or btype == C.MYSQL_TYPE_TIMESTAMP
+ local time = btype == C.MYSQL_TYPE_TIME or btype == C.MYSQL_TYPE_DATETIME or btype == C.MYSQL_TYPE_TIMESTAMP
+ assert(date or time, 'not a date/time type')
+ if self.null_flags[i-1] == 1 then return nil end
+ local tm = self.data[i]
+ return
+ date and tm.year or nil,
+ date and tm.month or nil,
+ date and tm.day or nil,
+ time and tm.hour or nil,
+ time and tm.minute or nil,
+ time and tm.second or nil,
+ time and tonumber(tm.second_part) or nil
+end
+
+function params:set_date(i, year, month, day, hour, min, sec, frac)
+ bind_check_range(self, i)
+ local tm = self.data[i]
+ local btype = tonumber(self.buffer[i-1].buffer_type)
+ local date = btype == C.MYSQL_TYPE_DATE or btype == C.MYSQL_TYPE_DATETIME or btype == C.MYSQL_TYPE_TIMESTAMP
+ local time = btype == C.MYSQL_TYPE_TIME or btype == C.MYSQL_TYPE_DATETIME or btype == C.MYSQL_TYPE_TIMESTAMP
+ assert(date or time, 'not a date/time type')
+ local tm = self.data[i]
+ tm.year = date and math.max(0, math.min(year or 0, 9999)) or 0
+ tm.month = date and math.max(1, math.min(month or 0, 12)) or 0
+ tm.day = date and math.max(1, math.min(day or 0, 31)) or 0
+ tm.hour = time and math.max(0, math.min(hour or 0, 59)) or 0
+ tm.minute = time and math.max(0, math.min(min or 0, 59)) or 0
+ tm.second = time and math.max(0, math.min(sec or 0, 59)) or 0
+ tm.second_part = time and math.max(0, math.min(frac or 0, 999999)) or 0
+ self.null_flags[i-1] = false
+end
+
+function params:set(i, v, size)
+ bind_check_range(self, i)
+ v = ptr(v)
+ if v == nil then
+ self.null_flags[i-1] = true
+ return
+ end
+ local btype = tonumber(self.buffer[i-1].buffer_type)
+ if btype == C.MYSQL_TYPE_NULL then
+ error('attempt to set a null type param')
+ elseif number_types[btype] then --this includes bit type which is LONGLONG
+ self.data[i][0] = v
+ self.null_flags[i-1] = false
+ elseif time_types[btype] then
+ self:set_date(i, v.year, v.month, v.day, v.hour, v.min, v.sec, v.frac)
+ else --var-sized types and raw bit blobs
+ size = size or #v
+ local bsize = tonumber(self.buffer[i-1].buffer_length)
+ assert(bsize >= size, 'string too long')
+ ffi.copy(self.data[i], v, size)
+ self.lengths[i-1] = size
+ self.null_flags[i-1] = false
+ end
+end
+
+function fields:get(i)
+ bind_check_range(self, i)
+ local btype = tonumber(self.buffer[i-1].buffer_type)
+ if btype == C.MYSQL_TYPE_NULL or self.null_flags[i-1] == 1 then
+ return nil
+ end
+ if number_types[btype] then
+ return self.data[i][0] --ffi converts this to a number or int64 type, which maches result:fetch() decoding
+ elseif time_types[btype] then
+ local t = self.data[i]
+ if t.time_type == C.MYSQL_TIMESTAMP_TIME then
+ return datetime{hour = t.hour, min = t.minute, sec = t.second, frac = tonumber(t.second_part)}
+ elseif t.time_type == C.MYSQL_TIMESTAMP_DATE then
+ return datetime{year = t.year, month = t.month, day = t.day}
+ elseif t.time_type == C.MYSQL_TIMESTAMP_DATETIME then
+ return datetime{year = t.year, month = t.month, day = t.day,
+ hour = t.hour, min = t.minute, sec = t.second, frac = tonumber(t.second_part)}
+ else
+ error'invalid time'
+ end
+ else
+ local sz = math.min(tonumber(self.buffer[i-1].buffer_length), tonumber(self.lengths[i-1]))
+ if btype == C.MYSQL_TYPE_BIT then
+ return parse_bit(self.data[i], sz)
+ else
+ return ffi.string(self.data[i], sz)
+ end
+ end
+end
+
+function fields:is_null(i) --returns true if the field is null
+ bind_check_range(self, i)
+ local btype = self.buffer[i-1].buffer_type
+ return btype == C.MYSQL_TYPE_NULL or self.null_flags[i-1] == 1
+end
+
+function fields:is_truncated(i) --returns true if the field value was truncated
+ bind_check_range(self, i)
+ return self.error_flags[i-1] == 1
+end
+
+local varsize_types = {
+ char = true,
+ binary = true,
+ varchar = true,
+ varbinary = true,
+ tinyblob = true,
+ tinytext = true,
+ blob = true,
+ text = true,
+ mediumblob = true,
+ mediumtext = true,
+ longblob = true,
+ longtext = true,
+ bit = true,
+ set = true,
+ enum = true,
+}
+
+function stmt.bind_result_types(stmt, maxsize)
+ local types = {}
+ local field_count = stmt:field_count()
+ local res = stmt:result_metadata()
+ if not res then return nil end
+ for i=1,field_count do
+ local ftype, size, unsigned, decimals = res:field_type(i)
+ if ftype == 'decimal' then
+ ftype = string.format('%s(%d,%d)', ftype, size-2, decimals)
+ elseif varsize_types[ftype] then
+ size = math.min(size, maxsize or 65535)
+ ftype = string.format('%s(%d)', ftype, size)
+ end
+ ftype = unsigned and ftype..' unsigned' or ftype
+ types[i] = ftype
+ end
+ res:free()
+ return types
+end
+
+function stmt.bind_params(stmt, ...)
+ local types = type((...)) == 'string' and {...} or ... or {}
+ assert(stmt:param_count() == #types, 'wrong number of param types')
+ local bb = params_bind_buffer(types)
+ stcheckz(stmt, C.mysql_stmt_bind_param(stmt, bb.buffer))
+ return bb
+end
+
+function stmt.bind_result(stmt, arg1, ...)
+ local types
+ if type(arg1) == 'string' then
+ types = {arg1, ...}
+ elseif type(arg1) == 'number' then
+ types = stmt:bind_result_types(arg1)
+ elseif arg1 then
+ types = arg1
+ else
+ types = stmt:bind_result_types()
+ end
+ assert(stmt:field_count() == #types, 'wrong number of field types')
+ local bb = fields_bind_buffer(types)
+ stcheckz(stmt, C.mysql_stmt_bind_result(stmt, bb.buffer))
+ return bb
+end
+
+--publish methods
+
+ffi.metatype('MYSQL', {__index = conn})
+ffi.metatype('MYSQL_RES', {__index = res})
+ffi.metatype('MYSQL_STMT', {__index = stmt})
+
+--publish classes (for introspection, not extending)
+
+M.conn = conn
+M.res = res
+M.stmt = stmt
+M.params = params
+M.fields = fields
+
+return M
diff --git a/worldmods/mysql_base/mysql/mysql.md b/worldmods/mysql_base/mysql/mysql.md
new file mode 100644
index 0000000..d0102d7
--- /dev/null
+++ b/worldmods/mysql_base/mysql/mysql.md
@@ -0,0 +1,548 @@
+---
+tagline: mysql database client
+---
+
+## `local mysql = require'mysql'`
+
+A complete, lightweight ffi binding of the mysql client library.
+
+> NOTE: binaries are in separate packages [libmysql] and [libmariadb].
+
+## Summary
+
+-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
+**[Initialization]**
+`mysql.config(['mysql'|'mariadb'|libname|clib]) -> mysql`
+**[Connections]**
+`mysql.connect(host, [user], [pass], [db], [charset], [port]) -> conn` connect to a mysql server
+`mysql.connect(options_t) -> conn` connect to a mysql server
+`conn:close()` close the connection
+**[Queries]**
+`conn:query(s)` execute a query
+`conn:escape(s) -> s` escape an SQL string
+**[Fetching results]**
+`conn:store_result() -> result` get a cursor for buffered read ([manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-store-result.html))
+`conn:use_result() -> result` get a cursor for unbuffered read ([manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-use-result.html))
+`result:fetch([mode[, row_t]]) -> true, v1, v2, ... | row_t | nil` fetch the next row from the result
+`result:rows([mode[, row_t]]) -> iterator() -> row_num, val1, val2, ...` row iterator
+`result:rows([mode[, row_t]]) -> iterator() -> row_num, row_t` row iterator
+`result:free()` free the cursor
+`result:row_count() -> n` number of rows
+`result:eof() -> true | false` check if no more rows
+`result:seek(row_number)` seek to row number
+**[Query info]**
+`conn:field_count() -> n` number of result fields in the executed query
+`conn:affected_rows() -> n` number of affected rows in the executed query
+`conn:insert_id() -> n` the id of the autoincrement column in the executed query
+`conn:errno() -> n` mysql error code (0 if no error) from the executed query
+`conn:sqlstate() -> s`
+`conn:warning_count() -> n` number of errors, warnings, and notes from executed query
+`conn:info() -> s`
+**[Field info]**
+`result:field_count() -> n` number of fields in the result
+`result:field_name(field_number) -> s` field name given field index
+`result:field_type(field_number) -> type, length, unsigned, decimals` field type given field index
+`result:field_info(field_number) -> info_t` field info table
+`result:fields() -> iterator() -> i, info_t` field info iterator
+**[Result bookmarks]**
+`result:tell() -> bookmark` bookmark the current row for later seek
+`result:seek(bookmark)` seek to a row bookmark
+**[Multiple statement queries]**
+`conn:next_result() -> true | false` skip to the next result set ([manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-next-result.html))
+`conn:more_results() -> true | false` are there more result sets?
+**[Prepared statements]**
+`conn:prepare(query) -> stmt` prepare a query for multiple executions
+`stmt:param_count() -> n` number of params
+`stmt:exec()` execute a prepared statement
+`stmt:store_result()` store all the resulted rows to the client
+`stmt:fetch() -> true | false | true, 'truncated'` fetch the next row
+`stmt:free_result()` free the current result buffers
+`stmt:close()` close the statement
+`stmt:next_result()` skip to the next result set
+`stmt:row_count() -> n` number of rows in the result, if the result was stored
+`stmt:affected_rows() -> n` number of affected rows after execution
+`stmt:insert_id() -> n` the id of the autoincrement column after execution
+`stmt:field_count() -> n` number of fields in the result after execution
+`stmt:errno() -> n` mysql error code, if any, from the executed statement
+`stmt:sqlstate() -> s`
+`stmt:result_metadata() -> result` get a result for accessing the field info
+`stmt:fields() -> iterator() -> i, info_t` iterate the result fields info
+`stmt:reset()` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-reset.html)
+`stmt:seek(row_number)` seek to row number
+`stmt:tell() -> bookmark` get a bookmark in the current result
+`stmt:seek(bookmark)` seek to a row bookmark in the current result
+**[Prepared statements I/O]**
+`stmt:bind_params(type1, ... | types_t) -> params` bind query parameters based on type definitions
+`params:set(i, number | int64_t | uint64_t | true | false)` set an integer, float or bit param
+`params:set(i, s[, size])` set a variable sized param
+`params:set(i, cdata, size)` set a variable sized param
+`params:set(i, {year=, month=, ...})` set a time/date param
+`params:set_date(i, [year], [month], [day], [hour], [min], [sec], [frac])` set a time/date param
+`stmt:write(param_number, data[, size])` send a long param in chunks
+`stmt:bind_result([type1, ... | types_t | maxsize]) -> fields` bind query result fields based on type definitions
+`fields:get(i) -> value` get the current row value of a field
+`fields:get_datetime(i) -> year, month, day, hour, min, sec, frac` get the value of a date/time field directly
+`fields:is_null(i) -> true | false` is field null?
+`fields:is_truncated(i) -> true | false` was field value truncated?
+**[Prepared statements settings]**
+`stmt:update_max_length() -> true | false` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:set_update_max_length(true | false)` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:cursor_type() -> mysql.C.MYSQL_CURSOR_TYPE_*` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:set_cursor_type('CURSOR_TYPE_...')` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:set_cursor_type(mysql.C.MYSQL_CURSOR_TYPE_...)` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:prefetch_rows() -> n` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+`stmt:set_prefetch_rows(stmt, n)` see [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html)
+**[Connection info]**
+`conn:set_charset(charset)` change the current charset
+`conn:select_db(dbname)` change the current database
+`conn:change_user(user, [pass], [db])` change the current user (and database)
+`conn:set_multiple_statements(true | false)` enable/disable support for multiple statements
+`conn:charset() -> s` get current charset's name
+`conn:charset_info() -> info_t` get info about the current charset
+`conn:ping() -> true | false` check if the connection is still alive
+`conn:thread_id() -> id`
+`conn:stat() -> s`
+`conn:server_info() -> s`
+`conn:host_info() -> s`
+`conn:server_version() -> n`
+`conn:proto_info() -> n`
+`conn:ssl_cipher() -> s`
+**[Transactions]**
+`conn:commit()` commit the current transaction
+`conn:rollback()` rollback the current transaction
+`conn:set_autocommit([true | false])` enable/disable autocommit on the current connection
+**[Reflection]**
+`conn:list_dbs([wildcard]) -> result` return info about databases as a result object
+`conn:list_tables([wildcard]) -> result` return info about tables as a result object
+`conn:list_processes() -> result` return info about processes as a result object
+**[Remote control]**
+`conn:kill(pid)` kill a connection based on process id
+`conn:shutdown([level])` shutdown the server
+`conn:refresh(options)` flush tables or caches
+`conn:dump_debug_info()` dump debug info in the log file
+**[Client library info]**
+`mysql.thread_safe() -> true | false` was the client library compiled as thread-safe?
+`mysql.client_info() -> s`
+`mysql.client_version() -> n`
+-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
+
+## Features
+
+ * covers all of the functionality provided by the mysql C API
+ * all data types are supported with options for conversion
+ * prepared statements, avoiding dynamic allocations and format conversions when fetching rows
+ * all C calls are checked for errors and Lua errors are raised
+ * all C objects are tied to Lua's garbage collector
+ * lightweight OOP-style API using only `ffi.metatype`
+ * no external dependencies
+
+## Example
+
+~~~{.lua}
+function print_help(search)
+ local mysql = require'mysql'
+
+ local conn = mysql.connect('localhost', 'root', nil, 'mysql', 'utf8')
+ conn:query("select name, description, example from help_topic where name like '" ..
+ conn:escape(search) .. "'")
+ local result = conn:store_result()
+
+ print('Found:')
+ for i,name in result:rows() do
+ print(' ' .. name)
+ end
+
+ print()
+ for i, name, description, example in result:rows() do
+ print(name)
+ print'-------------------------------------------'
+ print(description)
+ print'Example:'
+ print'-------------------------------------------'
+ print(example)
+ print()
+ end
+
+ result:free()
+ conn:close()
+end
+
+print_help'CONCAT%'
+~~~
+
+## Initialization
+
+### `mysql.config(['mysql'|'mariadb'|libname|clib]) -> mysql`
+
+Load the mysql client library to use (default is 'mysql').
+This function is called on every module-level function.
+Calling this function again is a no-op.
+
+## Connections
+
+### `mysql.connect(host, [user], [pass], [db], [charset], [port]) -> conn`
+### `mysql.connect(options_t) -> conn`
+
+Connect to a mysql server, optionally selecting a working database and charset.
+
+In the second form, `options_t` is a table that besides `host`, `user`, `pass`, `db`, `charset`, `port`
+can have the following fields:
+
+ * `unix_socket`: specify a unix socket filename to connect to
+ * `flags`: bit field corresponding to mysql [client_flag](http://dev.mysql.com/doc/refman/5.7/en/mysql-real-connect.html) parameter
+ * can be a table of form `{CLIENT_... = true | false, ...}`, or
+ * a number of form `bit.bor(mysql.C.CLIENT_..., ...)`
+ * `options`: a table of form `{MYSQL_OPT_... = value, ...}`, containing options per [mysql_options()](http://dev.mysql.com/doc/refman/5.7/en/mysql-options.html) (values are properly converted from Lua types)
+ * `attrs`: a table of form `{attr = value, ...}` containing attributes to be passed to the server per [mysql_options4()](http://dev.mysql.com/doc/refman/5.7/en/mysql-options4.html)
+ * `key`, `cert`, `ca`, `cpath`, `cipher`: parameters used to establish a [SSL connection](http://dev.mysql.com/doc/refman/5.7/en/mysql-ssl-set.html)
+
+### `conn:close()`
+
+Close a mysql connection freeing all associated resources (otherwise called when `conn` is garbage collected).
+
+## Queries
+
+### `conn:query(s)`
+
+Execute a query. If the query string contains multiple statements, only the first statement is executed
+(see the section on multiple statements).
+
+### `conn:escape(s) -> s`
+
+Escape a value to be safely embedded in SQL queries. Assumes the current charset.
+
+## Fetching results
+
+### `conn:store_result() -> result`
+
+Fetch all the rows in the current result set from the server and return a result object to read them one by one.
+
+### `conn:use_result() -> result`
+
+Return a result object that will fetch the rows in the current result set from the server on demand.
+
+### `result:fetch([mode[, row_t]]) -> true, v1, v2, ... | row_t | nil`
+
+Fetch and return the next row of values from the current result set. Returns nil if there are no more rows to fetch.
+
+ * the `mode` arg can contain any combination of the following letters:
+ * `"n"` - return values in a table with numeric indices as keys.
+ * `"a"` - return values in a table with field names as keys.
+ * `"s"` - do not convert numeric and time values to Lua types.
+ * the `row_t` arg is an optional table to store the row values in, instead of creating a new one on each fetch.
+ * options "a" and "n" can be combined to get a table with both numeric and field name indices.
+ * if `mode` is missing or if neither "a" nor "n" is specified, the values
+ are returned to the caller unpacked, after a first value that is always
+ true, to make it easy to distinguish between a valid `NULL` value in the
+ first column and eof.
+ * in "n" mode, the result table may contain `nil` values so `#row_t` and `ipairs(row_t)` are out; instead iterate from 1 to `result:field_count()`.
+ * in "a" mode, for fields with duplicate names only the last field will be present.
+ * if `mode` does not specify `"s"`, the following conversions are applied on the returned values:
+ * integer types are returned as Lua numbers, except bigint which is returned as an `int64_t` cdata (or `uint64` if unsigned).
+ * date/time types are returned as tables in the usual `os.date"*t"` format (date fields are missing for time-only types and viceversa).
+ * decimal/numeric types are returned as Lua strings.
+ * bit types are returned as Lua numbers, and as `uint64_t` for bit types larger than 48 bits.
+ * enum and set types are always returned as strings.
+
+### `result:rows([mode[, row_t]]) -> iterator() -> row_num, val1, val2, ...`
+### `result:rows([mode[, row_t]]) -> iterator() -> row_num, row_t`
+
+Convenience iterator for fetching (or refetching) all the rows from the current result set. The `mode` arg
+is the same as for `result:fetch()`, with the exception that in unpacked mode, the first `true` value is not present.
+
+### `result:free()`
+
+Free the result buffer (otherwise called when `result` is garbage collected).
+
+### `result:row_count() -> n`
+
+Return the number of rows in the current result set . This value is only correct if `result:store_result()` was
+previously called or if all the rows were fetched, in other words if `result:eof()` is true.
+
+### `result:eof() -> true | false`
+
+Check if there are no more rows to fetch. If `result:store_result()` was previously called, then all rows were
+already fetched, so `result:eof()` always returns `true` in this case.
+
+### `result:seek(row_number)`
+
+Seek back to a particular row number to refetch the rows from there.
+
+## Query info
+
+### `conn:field_count() -> n`
+### `conn:affected_rows() -> n`
+### `conn:insert_id() -> n`
+### `conn:errno() -> n`
+### `conn:sqlstate() -> s`
+### `conn:warning_count() -> n`
+### `conn:info() -> s`
+
+Return various pieces of information about the previously executed query.
+
+## Field info
+
+### `result:field_count() -> n`
+### `result:field_name(field_number) -> s`
+### `result:field_type(field_number) -> type, length, decimals, unsigned`
+### `result:field_info(field_number) -> info_t`
+### `result:fields() -> iterator() -> i, info_t`
+
+Return information about the fields (columns) in the current result set.
+
+## Result bookmarks
+
+### `result:tell() -> bookmark`
+
+Get a bookmark to the current row to be later seeked into with `seek()`.
+
+### `result:seek(bookmark)`
+
+Seek to a previous saved row bookmark, or to a specific row number, fetching more rows as needed.
+
+## Multiple statement queries
+
+### `conn:next_result() -> true | false`
+
+Skip over to the next result set in a multiple statement query, and make that the current result set.
+Return true if there more result sets after this one.
+
+### `conn:more_results() -> true | false`
+
+Check if there are more result sets after this one.
+
+## Prepared statements
+
+Prepared statements are a way to run queries and retrieve results more efficiently from the database, in particular:
+
+ * parametrized queries allow sending query parameters in their native format, avoiding having to convert values into strings and escaping those strings.
+ * running the same query multiple times with different parameters each time allows the server to reuse the parsed query and possibly the query plan between runs.
+ * fetching the result rows in preallocated buffers avoids dynamic allocation on each row fetch.
+
+The flow for prepared statements is like this:
+
+ * call `conn:prepare()` to prepare a query and get a statement object.
+ * call `stmt:bind_params()` and `stmt:bind_result()` to get the buffer objects for setting params and getting row values.
+ * run the query multiple times; each time:
+ * call `params:set()` for each param to set param values.
+ * call `stmt:exec()` to run the query.
+ * fetch the resulting rows one by one; for each row:
+ * call `stmt:fetch()` to get the next row (it returns false if it was the last row).
+ * call `fields:get()` to read the values of the fetched row.
+ * call `stmt:close()` to free the statement object and all the associated resources from the server and client.
+
+### `conn:prepare(query) -> stmt, params`
+
+Prepare a query for multiple execution and return a statement object.
+
+### `stmt:param_count() -> n`
+
+Number of parameters.
+
+### `stmt:exec()`
+
+Execute a prepared statement.
+
+### `stmt:store_result()`
+
+Fetch all the rows in the current result set from the server, otherwise the rows are fetched on demand.
+
+### `stmt:fetch() -> true | false | true, 'truncated'`
+
+Fetch the next row from the current result set. Use a binding buffer (see prepared statements I/O section)
+to get the row values. If present, second value indicates that at least one of the rows were truncated because
+the receiving buffer was too small for it.
+
+### `stmt:free_result()`
+
+Free the current result and all associated resources (otherwise the result is closed when the statement is closed).
+
+### `stmt:close()`
+
+Close a prepared statement and free all associated resources (otherwise the statement is closed when garbage collected).
+
+### `stmt:next_result()`
+
+Skip over to the next result set in a multiple statement query.
+
+### `stmt:row_count() -> n`
+### `stmt:affected_rows() -> n`
+### `stmt:insert_id() -> n`
+### `stmt:field_count() -> n`
+### `stmt:errno() -> n`
+### `stmt:sqlstate() -> s`
+### `stmt:result_metadata() -> result`
+### `stmt:fields() -> iterator() -> i, info_t`
+
+Return various pieces of information on the executed statement.
+
+### `stmt:reset()`
+
+See [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-reset.html).
+
+### `stmt:seek(row_number)`
+### `stmt:tell() -> bookmark`
+### `stmt:seek(bookmark)`
+
+Seek into the current result set.
+
+## Prepared statements I/O
+
+### `stmt:bind_params(type1, ... | types_t) -> params`
+
+Bind query parameters according to a list of type definitions (which can be given either packed or unpacked).
+Return a binding buffer object to be used for setting parameters.
+
+The types must be valid, fully specified SQL types, eg.
+
+ * `smallint unsigned` specifies a 16bit unsigned integer
+ * `bit(32)` specifies a 32bit bit field
+ * `varchar(200)` specifies a 200 byte varchar.
+
+### `params:set(i, number | int64_t | uint64_t | true | false)`
+### `params:set(i, s[, size])`
+### `params:set(i, cdata, size)`
+### `params:set(i, {year=, month=, ...})`
+### `params:set_date(i, [year], [month], [day], [hour], [min], [sec], [frac])`
+
+Set a parameter value.
+
+ * the first form is for setting integers and bit fields.
+ * the second and third forms are for setting variable-sized fields and decimal/numeric fields.
+ * the last forms are for setting date/time/datetime/timestamp fields.
+ * the null type cannot be set (raises an error if attempted).
+
+### `stmt:write(param_number, data[, size])`
+
+Send a parameter value in chunks (for long, var-sized values).
+
+### `stmt:bind_result([type1, ... | types_t | maxsize]) -> fields`
+
+Bind result fields according to a list of type definitions (same as for params).
+Return a binding buffer object to be used for getting row values.
+If no types are specified, appropriate type definitions will be created automatically as to minimize type conversions.
+Variable-sized fields will get a buffer sized according to data type's maximum allowed size
+and `maxsize` (which defaults to 64k).
+
+### `fields:get(i) -> value`
+### `fields:get_datetime(i) -> year, month, day, hour, min, sec, frac`
+
+Get a row value from the last fetched row. The same type conversions as for `result:fetch()` apply.
+
+### `fields:is_null(i) -> true | false`
+
+Check if a value is null without having to get it if it's not.
+
+### `fields:is_truncated(i) -> true | false`
+
+Check if a value was truncated due to insufficient buffer space.
+
+### `stmt:bind_result_types([maxsize]) -> types_t`
+
+Return the list of type definitions that describe the result of a prepared statement.
+
+## Prepared statements settings
+
+### `stmt:update_max_length() -> true | false`
+### `stmt:set_update_max_length(true | false)`
+### `stmt:cursor_type() -> mysql.C.MYSQL_CURSOR_TYPE_*`
+### `stmt:set_cursor_type('CURSOR_TYPE_...')`
+### `stmt:set_cursor_type(mysql.C.MYSQL_CURSOR_TYPE_...)`
+### `stmt:prefetch_rows() -> n`
+### `stmt:set_prefetch_rows(stmt, n)`
+
+See [manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-attr-set.html) for these.
+
+## Connection info
+
+### `conn:set_charset(charset)`
+
+Change the current charset.
+
+### `conn:select_db(dbname)`
+
+Change the current database.
+
+### `conn:change_user(user, [pass], [db])`
+
+Change the current user and optionally select a database.
+
+### `conn:set_multiple_statements(true | false)`
+
+Enable or disable support for query strings containing multiple statements separated by a semi-colon.
+
+### `conn:charset() -> s`
+
+Get the current charset.
+
+### `conn:charset_info() -> info_t`
+
+Return a table of information about the current charset.
+
+### `conn:ping() -> true | false`
+
+Check if the connection to the server is still alive.
+
+### `conn:thread_id() -> id`
+### `conn:stat() -> s`
+### `conn:server_info() -> s`
+### `conn:host_info() -> s`
+### `conn:server_version() -> n`
+### `conn:proto_info() -> n`
+### `conn:ssl_cipher() -> s`
+
+Return various pieces of information about the connection and server.
+
+## Transactions
+
+### `conn:commit()`
+### `conn:rollback()`
+
+Commit/rollback the current transaction.
+
+### `conn:set_autocommit([true | false])`
+
+Set autocommit on the connection (set to true if no argument is given).
+
+## Reflection
+
+### `conn:list_dbs([wildcard]) -> result`
+### `conn:list_tables([wildcard]) -> result`
+### `conn:list_processes() -> result`
+
+Return information about databases, tables and proceses as a stored result object that can be iterated etc.
+using the methods of result objects. The optional `wild` parameter may contain the wildcard characters
+`"%"` or `"_"`, similar to executing the query `SHOW DATABASES [LIKE wild]`.
+
+## Remote control
+
+### `conn:kill(pid)`
+
+Kill a connection with a specific `pid`.
+
+### `conn:shutdown([level])`
+
+Shutdown the server. `SHUTDOWN` priviledge needed. The level argument is reserved for future versions of mysql.
+
+### `conn:refresh(options)`
+
+Flush tables or caches, or resets replication server information. `RELOAD` priviledge needed. Options are either
+a table of form `{REFRESH_... = true | false, ...}` or a number of form `bit.bor(mysql.C.MYSQL_REFRESH_*, ...)` and
+they are as described in the [mysql manual](http://dev.mysql.com/doc/refman/5.7/en/mysql-refresh.html).
+
+### `conn:dump_debug_info()`
+
+Instruct the server to dump debug info in the log file. `SUPER` priviledge needed.
+
+## Client library info
+
+### `mysql.thread_safe() -> true | false`
+### `mysql.client_info() -> s`
+### `mysql.client_version() -> n`
+
+----
+
+## TODO
+
+ * reader function for getting large blobs in chunks using
+ mysql_stmt_fetch_column: `stmt:chunks(i[, bufsize])` or `stmt:read()` ?
diff --git a/worldmods/mysql_base/mysql/mysql_h.lua b/worldmods/mysql_base/mysql/mysql_h.lua
new file mode 100644
index 0000000..474ad5a
--- /dev/null
+++ b/worldmods/mysql_base/mysql/mysql_h.lua
@@ -0,0 +1,571 @@
+
+--result of `cpp mysql.h` with lots of cleanup and defines from other headers.
+--Written by Cosmin Apreutesei. MySQL Connector/C 6.1.
+
+--NOTE: MySQL Connector/C is GPL software. Is this "derived work" then?
+
+local ffi = require'ffi'
+
+ffi.cdef[[
+
+typedef char my_bool;
+typedef unsigned long long my_ulonglong;
+
+enum {
+ MYSQL_PORT = 3306,
+ MYSQL_ERRMSG_SIZE = 512,
+
+ // status return codes
+ MYSQL_NO_DATA = 100,
+ MYSQL_DATA_TRUNCATED = 101
+};
+
+// ----------------------------------------------------------- error constants
+
+// NOTE: added MYSQL_ prefix to these.
+enum mysql_error_code {
+ MYSQL_CR_UNKNOWN_ERROR = 2000,
+ MYSQL_CR_SOCKET_CREATE_ERROR = 2001,
+ MYSQL_CR_CONNECTION_ERROR = 2002,
+ MYSQL_CR_CONN_HOST_ERROR = 2003,
+ MYSQL_CR_IPSOCK_ERROR = 2004,
+ MYSQL_CR_UNKNOWN_HOST = 2005,
+ MYSQL_CR_SERVER_GONE_ERROR = 2006,
+ MYSQL_CR_VERSION_ERROR = 2007,
+ MYSQL_CR_OUT_OF_MEMORY = 2008,
+ MYSQL_CR_WRONG_HOST_INFO = 2009,
+ MYSQL_CR_LOCALHOST_CONNECTION = 2010,
+ MYSQL_CR_TCP_CONNECTION = 2011,
+ MYSQL_CR_SERVER_HANDSHAKE_ERR = 2012,
+ MYSQL_CR_SERVER_LOST = 2013,
+ MYSQL_CR_COMMANDS_OUT_OF_SYNC = 2014,
+ MYSQL_CR_NAMEDPIPE_CONNECTION = 2015,
+ MYSQL_CR_NAMEDPIPEWAIT_ERROR = 2016,
+ MYSQL_CR_NAMEDPIPEOPEN_ERROR = 2017,
+ MYSQL_CR_NAMEDPIPESETSTATE_ERROR = 2018,
+ MYSQL_CR_CANT_READ_CHARSET = 2019,
+ MYSQL_CR_NET_PACKET_TOO_LARGE = 2020,
+ MYSQL_CR_EMBEDDED_CONNECTION = 2021,
+ MYSQL_CR_PROBE_SLAVE_STATUS = 2022,
+ MYSQL_CR_PROBE_SLAVE_HOSTS = 2023,
+ MYSQL_CR_PROBE_SLAVE_CONNECT = 2024,
+ MYSQL_CR_PROBE_MASTER_CONNECT = 2025,
+ MYSQL_CR_SSL_CONNECTION_ERROR = 2026,
+ MYSQL_CR_MALFORMED_PACKET = 2027,
+ MYSQL_CR_WRONG_LICENSE = 2028,
+
+ /* new 4.1 error codes */
+ MYSQL_CR_NULL_POINTER = 2029,
+ MYSQL_CR_NO_PREPARE_STMT = 2030,
+ MYSQL_CR_PARAMS_NOT_BOUND = 2031,
+ MYSQL_CR_DATA_TRUNCATED = 2032,
+ MYSQL_CR_NO_PARAMETERS_EXISTS = 2033,
+ MYSQL_CR_INVALID_PARAMETER_NO = 2034,
+ MYSQL_CR_INVALID_BUFFER_USE = 2035,
+ MYSQL_CR_UNSUPPORTED_PARAM_TYPE = 2036,
+
+ MYSQL_CR_SHARED_MEMORY_CONNECTION = 2037,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_MAP_ERROR = 2041,
+ MYSQL_CR_SHARED_MEMORY_FILE_MAP_ERROR = 2042,
+ MYSQL_CR_SHARED_MEMORY_MAP_ERROR = 2043,
+ MYSQL_CR_SHARED_MEMORY_EVENT_ERROR = 2044,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045,
+ MYSQL_CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046,
+ MYSQL_CR_CONN_UNKNOW_PROTOCOL = 2047,
+ MYSQL_CR_INVALID_CONN_HANDLE = 2048,
+ MYSQL_CR_SECURE_AUTH = 2049,
+ MYSQL_CR_FETCH_CANCELED = 2050,
+ MYSQL_CR_NO_DATA = 2051,
+ MYSQL_CR_NO_STMT_METADATA = 2052,
+ MYSQL_CR_NO_RESULT_SET = 2053,
+ MYSQL_CR_NOT_IMPLEMENTED = 2054,
+ MYSQL_CR_SERVER_LOST_EXTENDED = 2055,
+ MYSQL_CR_STMT_CLOSED = 2056,
+ MYSQL_CR_NEW_STMT_METADATA = 2057,
+ MYSQL_CR_ALREADY_CONNECTED = 2058,
+ MYSQL_CR_AUTH_PLUGIN_CANNOT_LOAD = 2059,
+ MYSQL_CR_DUPLICATE_CONNECTION_ATTR = 2060,
+ MYSQL_CR_AUTH_PLUGIN_ERR = 2061
+};
+
+// ------------------------------------------------------------ client library
+
+unsigned int mysql_thread_safe(void); // is the client library thread safe?
+const char *mysql_get_client_info(void);
+unsigned long mysql_get_client_version(void);
+
+// --------------------------------------------------------------- connections
+
+typedef struct MYSQL_ MYSQL;
+
+MYSQL * mysql_init(MYSQL *mysql);
+
+enum mysql_protocol_type
+{
+ MYSQL_PROTOCOL_DEFAULT, MYSQL_PROTOCOL_TCP, MYSQL_PROTOCOL_SOCKET,
+ MYSQL_PROTOCOL_PIPE, MYSQL_PROTOCOL_MEMORY
+};
+enum mysql_option
+{
+ MYSQL_OPT_CONNECT_TIMEOUT, MYSQL_OPT_COMPRESS, MYSQL_OPT_NAMED_PIPE,
+ MYSQL_INIT_COMMAND, MYSQL_READ_DEFAULT_FILE, MYSQL_READ_DEFAULT_GROUP,
+ MYSQL_SET_CHARSET_DIR, MYSQL_SET_CHARSET_NAME, MYSQL_OPT_LOCAL_INFILE,
+ MYSQL_OPT_PROTOCOL, MYSQL_SHARED_MEMORY_BASE_NAME, MYSQL_OPT_READ_TIMEOUT,
+ MYSQL_OPT_WRITE_TIMEOUT, MYSQL_OPT_USE_RESULT,
+ MYSQL_OPT_USE_REMOTE_CONNECTION, MYSQL_OPT_USE_EMBEDDED_CONNECTION,
+ MYSQL_OPT_GUESS_CONNECTION, MYSQL_SET_CLIENT_IP, MYSQL_SECURE_AUTH,
+ MYSQL_REPORT_DATA_TRUNCATION, MYSQL_OPT_RECONNECT,
+ MYSQL_OPT_SSL_VERIFY_SERVER_CERT, MYSQL_PLUGIN_DIR, MYSQL_DEFAULT_AUTH,
+ MYSQL_OPT_BIND,
+ MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT,
+ MYSQL_OPT_SSL_CA, MYSQL_OPT_SSL_CAPATH, MYSQL_OPT_SSL_CIPHER,
+ MYSQL_OPT_SSL_CRL, MYSQL_OPT_SSL_CRLPATH,
+ MYSQL_OPT_CONNECT_ATTR_RESET, MYSQL_OPT_CONNECT_ATTR_ADD,
+ MYSQL_OPT_CONNECT_ATTR_DELETE,
+ MYSQL_SERVER_PUBLIC_KEY,
+ MYSQL_ENABLE_CLEARTEXT_PLUGIN,
+ MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS
+};
+int mysql_options(MYSQL *mysql, enum mysql_option option, const void *arg);
+int mysql_options4(MYSQL *mysql, enum mysql_option option, const void *arg1, const void *arg2);
+
+// NOTE: added MYSQL_ prefix to these. Also, these are bit flags not exclusive enum values.
+enum {
+ MYSQL_CLIENT_LONG_PASSWORD = 1, /* new more secure passwords */
+ MYSQL_CLIENT_FOUND_ROWS = 2, /* Found instead of affected rows */
+ MYSQL_CLIENT_LONG_FLAG = 4, /* Get all column flags */
+ MYSQL_CLIENT_CONNECT_WITH_DB = 8, /* One can specify db on connect */
+ MYSQL_CLIENT_NO_SCHEMA = 16, /* Don't allow database.table.column */
+ MYSQL_CLIENT_COMPRESS = 32, /* Can use compression protocol */
+ MYSQL_CLIENT_ODBC = 64, /* ODBC client */
+ MYSQL_CLIENT_LOCAL_FILES = 128, /* Can use LOAD DATA LOCAL */
+ MYSQL_CLIENT_IGNORE_SPACE = 256, /* Ignore spaces before '(' */
+ MYSQL_CLIENT_PROTOCOL_41 = 512, /* New 4.1 protocol */
+ MYSQL_CLIENT_INTERACTIVE = 1024, /* This is an interactive client */
+ MYSQL_CLIENT_SSL = 2048, /* Switch to SSL after handshake */
+ MYSQL_CLIENT_IGNORE_SIGPIPE = 4096, /* IGNORE sigpipes */
+ MYSQL_CLIENT_TRANSACTIONS = 8192, /* Client knows about transactions */
+ MYSQL_CLIENT_RESERVED = 16384, /* Old flag for 4.1 protocol */
+ MYSQL_CLIENT_SECURE_CONNECTION = (1U << 15), /* New 4.1 authentication */
+ MYSQL_CLIENT_MULTI_STATEMENTS = (1U << 16), /* Enable/disable multi-stmt support */
+ MYSQL_CLIENT_MULTI_RESULTS = (1U << 17), /* Enable/disable multi-results */
+ MYSQL_CLIENT_PS_MULTI_RESULTS = (1U << 18), /* Multi-results in PS-protocol */
+ MYSQL_CLIENT_PLUGIN_AUTH = (1U << 19), /* Client supports plugin authentication */
+ MYSQL_CLIENT_CONNECT_ATTRS = (1U << 20), /* Client supports connection attributes */
+
+ /* Enable authentication response packet to be larger than 255 bytes. */
+ MYSQL_CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = (1U << 21),
+
+ /* Don't close the connection for a connection with expired password. */
+ MYSQL_CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS = (1U << 22),
+
+ MYSQL_CLIENT_SSL_VERIFY_SERVER_CERT = (1U << 30),
+ MYSQL_CLIENT_REMEMBER_OPTIONS = (1U << 31)
+};
+MYSQL * mysql_real_connect(MYSQL *mysql, const char *host,
+ const char *user,
+ const char *passwd,
+ const char *db,
+ unsigned int port,
+ const char *unix_socket,
+ unsigned long clientflag);
+
+void mysql_close(MYSQL *sock);
+
+int mysql_set_character_set(MYSQL *mysql, const char *csname);
+
+int mysql_select_db(MYSQL *mysql, const char *db);
+
+my_bool mysql_change_user(MYSQL *mysql, const char *user, const char *passwd,
+ const char *db);
+
+my_bool mysql_ssl_set(MYSQL *mysql, const char *key,
+ const char *cert, const char *ca,
+ const char *capath, const char *cipher);
+
+enum enum_mysql_set_option
+{
+ MYSQL_OPTION_MULTI_STATEMENTS_ON,
+ MYSQL_OPTION_MULTI_STATEMENTS_OFF
+};
+int mysql_set_server_option(MYSQL *mysql, enum enum_mysql_set_option option);
+
+// ----------------------------------------------------------- connection info
+
+const char * mysql_character_set_name(MYSQL *mysql);
+
+typedef struct character_set
+{
+ unsigned int number;
+ unsigned int state;
+ const char *csname;
+ const char *name;
+ const char *comment;
+ const char *dir;
+ unsigned int mbminlen;
+ unsigned int mbmaxlen;
+} MY_CHARSET_INFO;
+void mysql_get_character_set_info(MYSQL *mysql, MY_CHARSET_INFO *charset);
+
+int mysql_ping(MYSQL *mysql);
+unsigned long mysql_thread_id(MYSQL *mysql);
+const char * mysql_stat(MYSQL *mysql);
+const char * mysql_get_server_info(MYSQL *mysql);
+const char * mysql_get_host_info(MYSQL *mysql);
+unsigned long mysql_get_server_version(MYSQL *mysql);
+unsigned int mysql_get_proto_info(MYSQL *mysql);
+const char * mysql_get_ssl_cipher(MYSQL *mysql);
+
+// -------------------------------------------------------------- transactions
+
+my_bool mysql_commit(MYSQL * mysql);
+my_bool mysql_rollback(MYSQL * mysql);
+my_bool mysql_autocommit(MYSQL * mysql, my_bool auto_mode);
+
+// ------------------------------------------------------------------- queries
+
+unsigned long mysql_real_escape_string(MYSQL *mysql, char *to,
+ const char *from, unsigned long length);
+int mysql_real_query(MYSQL *mysql, const char *q, unsigned long length);
+
+// ---------------------------------------------------------------- query info
+
+unsigned int mysql_field_count(MYSQL *mysql);
+my_ulonglong mysql_affected_rows(MYSQL *mysql);
+my_ulonglong mysql_insert_id(MYSQL *mysql);
+unsigned int mysql_errno(MYSQL *mysql);
+const char * mysql_error(MYSQL *mysql);
+const char * mysql_sqlstate(MYSQL *mysql);
+unsigned int mysql_warning_count(MYSQL *mysql);
+const char * mysql_info(MYSQL *mysql);
+
+// ------------------------------------------------------------- query results
+
+int mysql_next_result(MYSQL *mysql);
+my_bool mysql_more_results(MYSQL *mysql);
+
+// NOTE: normally we would've made this an opaque handle, but we need to expose
+// the connection handle from it so we can report errors for unbuffered reads.
+typedef struct st_mysql_res {
+ my_ulonglong __row_count;
+ void *__fields;
+ void *__data;
+ void *__data_cursor;
+ void *__lengths;
+ MYSQL *conn; /* for unbuffered reads */
+} MYSQL_RES;
+
+MYSQL_RES *mysql_store_result(MYSQL *mysql);
+MYSQL_RES *mysql_use_result(MYSQL *mysql);
+void mysql_free_result(MYSQL_RES *result);
+
+my_ulonglong mysql_num_rows(MYSQL_RES *res);
+unsigned int mysql_num_fields(MYSQL_RES *res);
+my_bool mysql_eof(MYSQL_RES *res);
+
+unsigned long * mysql_fetch_lengths(MYSQL_RES *result);
+typedef char **MYSQL_ROW;
+MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
+
+void mysql_data_seek(MYSQL_RES *result, my_ulonglong offset);
+
+typedef struct MYSQL_ROWS_ MYSQL_ROWS;
+typedef MYSQL_ROWS *MYSQL_ROW_OFFSET;
+MYSQL_ROW_OFFSET mysql_row_tell(MYSQL_RES *res);
+MYSQL_ROW_OFFSET mysql_row_seek(MYSQL_RES *result, MYSQL_ROW_OFFSET offset);
+
+// ---------------------------------------------------------- query field info
+
+enum enum_field_types {
+ MYSQL_TYPE_DECIMAL, MYSQL_TYPE_TINY,
+ MYSQL_TYPE_SHORT, MYSQL_TYPE_LONG,
+ MYSQL_TYPE_FLOAT, MYSQL_TYPE_DOUBLE,
+ MYSQL_TYPE_NULL, MYSQL_TYPE_TIMESTAMP,
+ MYSQL_TYPE_LONGLONG,MYSQL_TYPE_INT24,
+ MYSQL_TYPE_DATE, MYSQL_TYPE_TIME,
+ MYSQL_TYPE_DATETIME, MYSQL_TYPE_YEAR,
+ MYSQL_TYPE_NEWDATE, MYSQL_TYPE_VARCHAR,
+ MYSQL_TYPE_BIT,
+ MYSQL_TYPE_TIMESTAMP2,
+ MYSQL_TYPE_DATETIME2,
+ MYSQL_TYPE_TIME2,
+ MYSQL_TYPE_NEWDECIMAL=246,
+ MYSQL_TYPE_ENUM=247,
+ MYSQL_TYPE_SET=248,
+ MYSQL_TYPE_TINY_BLOB=249,
+ MYSQL_TYPE_MEDIUM_BLOB=250,
+ MYSQL_TYPE_LONG_BLOB=251,
+ MYSQL_TYPE_BLOB=252,
+ MYSQL_TYPE_VAR_STRING=253,
+ MYSQL_TYPE_STRING=254,
+ MYSQL_TYPE_GEOMETRY=255
+};
+
+// NOTE: added MYSQL_ prefix to these. Also, these are bit flags, not exclusive enum values.
+enum {
+ MYSQL_NOT_NULL_FLAG = 1, /* Field can't be NULL */
+ MYSQL_PRI_KEY_FLAG = 2, /* Field is part of a primary key */
+ MYSQL_UNIQUE_KEY_FLAG = 4, /* Field is part of a unique key */
+ MYSQL_MULTIPLE_KEY_FLAG = 8, /* Field is part of a key */
+ MYSQL_BLOB_FLAG = 16, /* Field is a blob */
+ MYSQL_UNSIGNED_FLAG = 32, /* Field is unsigned */
+ MYSQL_ZEROFILL_FLAG = 64, /* Field is zerofill */
+ MYSQL_BINARY_FLAG = 128, /* Field is binary */
+
+ /* The following are only sent to new clients */
+ MYSQL_ENUM_FLAG = 256, /* field is an enum */
+ MYSQL_AUTO_INCREMENT_FLAG = 512, /* field is a autoincrement field */
+ MYSQL_TIMESTAMP_FLAG = 1024, /* Field is a timestamp */
+ MYSQL_SET_FLAG = 2048, /* field is a set */
+ MYSQL_NO_DEFAULT_VALUE_FLAG = 4096, /* Field doesn't have default value */
+ MYSQL_ON_UPDATE_NOW_FLAG = 8192, /* Field is set to NOW on UPDATE */
+ MYSQL_NUM_FLAG = 32768, /* Field is num (for clients) */
+ MYSQL_PART_KEY_FLAG = 16384, /* Intern; Part of some key */
+ MYSQL_GROUP_FLAG = 32768, /* Intern: Group field */
+ MYSQL_UNIQUE_FLAG = 65536, /* Intern: Used by sql_yacc */
+ MYSQL_BINCMP_FLAG = 131072, /* Intern: Used by sql_yacc */
+ MYSQL_GET_FIXED_FIELDS_FLAG = (1 << 18), /* Used to get fields in item tree */
+ MYSQL_FIELD_IN_PART_FUNC_FLAG = (1 << 19) /* Field part of partition func */
+};
+
+typedef struct st_mysql_field {
+ char *name;
+ char *org_name;
+ char *table;
+ char *org_table;
+ char *db;
+ char *catalog;
+ char *def;
+ unsigned long length;
+ unsigned long max_length;
+ unsigned int name_length;
+ unsigned int org_name_length;
+ unsigned int table_length;
+ unsigned int org_table_length;
+ unsigned int db_length;
+ unsigned int catalog_length;
+ unsigned int def_length;
+ unsigned int flags;
+ unsigned int decimals;
+ unsigned int charsetnr;
+ enum enum_field_types type;
+ void *extension;
+} MYSQL_FIELD;
+
+MYSQL_FIELD *mysql_fetch_field_direct(MYSQL_RES *res, unsigned int fieldnr);
+
+// ---------------------------------------------------------------- reflection
+
+MYSQL_RES *mysql_list_dbs(MYSQL *mysql, const char *wild);
+MYSQL_RES *mysql_list_tables(MYSQL *mysql, const char *wild);
+MYSQL_RES *mysql_list_processes(MYSQL *mysql);
+
+// ------------------------------------------------------------ remote control
+
+int mysql_kill(MYSQL *mysql, unsigned long pid);
+
+// NOTE: added MYSQL_ prefix.
+enum mysql_enum_shutdown_level {
+ MYSQL_SHUTDOWN_DEFAULT = 0,
+ MYSQL_SHUTDOWN_WAIT_CONNECTIONS = 1,
+ MYSQL_SHUTDOWN_WAIT_TRANSACTIONS = 2,
+ MYSQL_SHUTDOWN_WAIT_UPDATES = 8,
+ MYSQL_SHUTDOWN_WAIT_ALL_BUFFERS = 16,
+ MYSQL_SHUTDOWN_WAIT_CRITICAL_BUFFERS = 17,
+ MYSQL_KILL_QUERY = 254,
+ MYSQL_KILL_CONNECTION = 255
+};
+int mysql_shutdown(MYSQL *mysql, enum mysql_enum_shutdown_level shutdown_level); // needs SHUTDOWN priviledge
+
+// NOTE: added MYSQL_ prefix. not really enum values either, just bit flags.
+enum {
+ MYSQL_REFRESH_GRANT = 1, /* Refresh grant tables */
+ MYSQL_REFRESH_LOG = 2, /* Start on new log file */
+ MYSQL_REFRESH_TABLES = 4, /* close all tables */
+ MYSQL_REFRESH_HOSTS = 8, /* Flush host cache */
+ MYSQL_REFRESH_STATUS = 16, /* Flush status variables */
+ MYSQL_REFRESH_THREADS = 32, /* Flush thread cache */
+ MYSQL_REFRESH_SLAVE = 64, /* Reset master info and restart slave thread */
+ MYSQL_REFRESH_MASTER = 128, /* Remove all bin logs in the index and truncate the index */
+ MYSQL_REFRESH_ERROR_LOG = 256, /* Rotate only the erorr log */
+ MYSQL_REFRESH_ENGINE_LOG = 512, /* Flush all storage engine logs */
+ MYSQL_REFRESH_BINARY_LOG = 1024, /* Flush the binary log */
+ MYSQL_REFRESH_RELAY_LOG = 2048, /* Flush the relay log */
+ MYSQL_REFRESH_GENERAL_LOG = 4096, /* Flush the general log */
+ MYSQL_REFRESH_SLOW_LOG = 8192, /* Flush the slow query log */
+
+ /* The following can't be set with mysql_refresh() */
+ MYSQL_REFRESH_READ_LOCK = 16384, /* Lock tables for read */
+ MYSQL_REFRESH_FAST = 32768, /* Intern flag */
+
+ /* RESET (remove all queries) from query cache */
+ MYSQL_REFRESH_QUERY_CACHE = 65536,
+ MYSQL_REFRESH_QUERY_CACHE_FREE = 0x20000, /* pack query cache */
+ MYSQL_REFRESH_DES_KEY_FILE = 0x40000,
+ MYSQL_REFRESH_USER_RESOURCES = 0x80000,
+ MYSQL_REFRESH_FOR_EXPORT = 0x100000, /* FLUSH TABLES ... FOR EXPORT */
+};
+int mysql_refresh(MYSQL *mysql, unsigned int refresh_options); // needs RELOAD priviledge
+int mysql_dump_debug_info(MYSQL *mysql); // needs SUPER priviledge
+
+// ------------------------------------------------------- prepared statements
+
+typedef struct MYSQL_STMT_ MYSQL_STMT;
+
+MYSQL_STMT * mysql_stmt_init(MYSQL *mysql);
+my_bool mysql_stmt_close(MYSQL_STMT * stmt);
+
+int mysql_stmt_prepare(MYSQL_STMT *stmt, const char *query, unsigned long length);
+int mysql_stmt_execute(MYSQL_STMT *stmt);
+
+int mysql_stmt_next_result(MYSQL_STMT *stmt);
+int mysql_stmt_store_result(MYSQL_STMT *stmt);
+my_bool mysql_stmt_free_result(MYSQL_STMT *stmt);
+
+MYSQL_RES *mysql_stmt_result_metadata(MYSQL_STMT *stmt);
+my_ulonglong mysql_stmt_num_rows(MYSQL_STMT *stmt);
+my_ulonglong mysql_stmt_affected_rows(MYSQL_STMT *stmt);
+my_ulonglong mysql_stmt_insert_id(MYSQL_STMT *stmt);
+unsigned int mysql_stmt_field_count(MYSQL_STMT *stmt);
+
+unsigned int mysql_stmt_errno(MYSQL_STMT * stmt);
+const char *mysql_stmt_error(MYSQL_STMT * stmt);
+const char *mysql_stmt_sqlstate(MYSQL_STMT * stmt);
+
+int mysql_stmt_fetch(MYSQL_STMT *stmt);
+my_bool mysql_stmt_reset(MYSQL_STMT * stmt);
+
+void mysql_stmt_data_seek(MYSQL_STMT *stmt, my_ulonglong offset);
+
+MYSQL_ROW_OFFSET mysql_stmt_row_tell(MYSQL_STMT *stmt);
+MYSQL_ROW_OFFSET mysql_stmt_row_seek(MYSQL_STMT *stmt, MYSQL_ROW_OFFSET offset);
+
+// NOTE: added MYSQL_ prefix to these.
+enum enum_cursor_type
+{
+ MYSQL_CURSOR_TYPE_NO_CURSOR= 0,
+ MYSQL_CURSOR_TYPE_READ_ONLY= 1,
+ MYSQL_CURSOR_TYPE_FOR_UPDATE= 2,
+ MYSQL_CURSOR_TYPE_SCROLLABLE= 4
+};
+
+enum enum_stmt_attr_type
+{
+ STMT_ATTR_UPDATE_MAX_LENGTH,
+ STMT_ATTR_CURSOR_TYPE,
+ STMT_ATTR_PREFETCH_ROWS
+};
+my_bool mysql_stmt_attr_set(MYSQL_STMT *stmt, enum enum_stmt_attr_type attr_type, const void *attr);
+my_bool mysql_stmt_attr_get(MYSQL_STMT *stmt, enum enum_stmt_attr_type attr_type, void *attr);
+
+my_bool mysql_stmt_send_long_data(MYSQL_STMT *stmt,
+ unsigned int param_number,
+ const char *data,
+ unsigned long length);
+
+// -------------------------------------------- prepared statements / bindings
+
+enum enum_mysql_timestamp_type
+{
+ MYSQL_TIMESTAMP_NONE= -2, MYSQL_TIMESTAMP_ERROR= -1,
+ MYSQL_TIMESTAMP_DATE= 0, MYSQL_TIMESTAMP_DATETIME= 1, MYSQL_TIMESTAMP_TIME= 2
+};
+typedef struct st_mysql_time
+{
+ unsigned int year, month, day, hour, minute, second;
+ unsigned long second_part; /**< microseconds */
+ my_bool neg;
+ enum enum_mysql_timestamp_type time_type;
+} MYSQL_TIME;
+
+unsigned long mysql_stmt_param_count(MYSQL_STMT * stmt);
+
+typedef struct NET_ NET;
+typedef struct st_mysql_bind
+{
+ unsigned long *length;
+ my_bool *is_null;
+ void *buffer;
+ my_bool *error;
+ unsigned char *row_ptr;
+ void (*store_param_func)(NET *net, struct st_mysql_bind *param);
+ void (*fetch_result)(struct st_mysql_bind *, MYSQL_FIELD *,
+ unsigned char **row);
+ void (*skip_result)(struct st_mysql_bind *, MYSQL_FIELD *,
+ unsigned char **row);
+ unsigned long buffer_length;
+ unsigned long offset;
+ unsigned long length_value;
+ unsigned int param_number;
+ unsigned int pack_length;
+ enum enum_field_types buffer_type;
+ my_bool error_value;
+ my_bool is_unsigned;
+ my_bool long_data_used;
+ my_bool is_null_value;
+ void *extension;
+} MYSQL_BIND;
+
+my_bool mysql_stmt_bind_param(MYSQL_STMT * stmt, MYSQL_BIND * bnd);
+my_bool mysql_stmt_bind_result(MYSQL_STMT * stmt, MYSQL_BIND * bnd);
+
+int mysql_stmt_fetch_column(MYSQL_STMT *stmt, MYSQL_BIND *bind_arg,
+ unsigned int column,
+ unsigned long offset);
+
+// ---------------------------------------------- LOAD DATA LOCAL INFILE hooks
+
+void mysql_set_local_infile_handler(MYSQL *mysql,
+ int (*local_infile_init)(void **, const char *, void *),
+ int (*local_infile_read)(void *, char *, unsigned int),
+ void (*local_infile_end)(void *),
+ int (*local_infile_error)(void *, char*, unsigned int),
+ void *);
+void mysql_set_local_infile_default(MYSQL *mysql);
+
+// ----------------------------------------------------- mysql proxy scripting
+
+my_bool mysql_read_query_result(MYSQL *mysql);
+
+// ----------------------------------------------------------------- debugging
+
+void mysql_debug(const char *debug);
+
+// ------------------------------------------------ present but not documented
+
+int mysql_server_init(int argc, char **argv, char **groups);
+void mysql_server_end(void);
+char *get_tty_password(const char *opt_message);
+void myodbc_remove_escape(MYSQL *mysql, char *name);
+my_bool mysql_embedded(void);
+int mysql_send_query(MYSQL *mysql, const char *q, unsigned long length);
+
+// ------------------------------------------------------- redundant functions
+
+my_bool mysql_thread_init(void); // called anyway
+void mysql_thread_end(void); // called anyway
+const char *mysql_errno_to_sqlstate(unsigned int mysql_errno); // use mysql_sqlstate
+unsigned long mysql_hex_string(char *to, const char *from,
+ unsigned long from_length); // bad taste
+
+// redundant ways to get field info.
+// we use use mysql_field_count and mysql_fetch_field_direct instead.
+MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result);
+MYSQL_FIELD *mysql_fetch_fields(MYSQL_RES *res);
+typedef unsigned int MYSQL_FIELD_OFFSET;
+MYSQL_FIELD_OFFSET mysql_field_tell(MYSQL_RES *res);
+MYSQL_FIELD_OFFSET mysql_field_seek(MYSQL_RES *result, MYSQL_FIELD_OFFSET offset);
+MYSQL_RES *mysql_stmt_param_metadata(MYSQL_STMT *stmt);
+
+// ------------------------------------------------------ deprecated functions
+
+unsigned long mysql_escape_string(char *to, const char *from,
+ unsigned long from_length); // use mysql_real_escape_string
+int mysql_query(MYSQL *mysql, const char *q); // use mysql_real_query
+MYSQL_RES *mysql_list_fields(MYSQL *mysql, const char *table,
+ const char *wild); // use "SHOW COLUMNS FROM table"
+
+]]
diff --git a/worldmods/mysql_base/mysql/mysql_print.lua b/worldmods/mysql_base/mysql/mysql_print.lua
new file mode 100644
index 0000000..bb1c3d3
--- /dev/null
+++ b/worldmods/mysql_base/mysql/mysql_print.lua
@@ -0,0 +1,144 @@
+--mysql table pretty printing
+
+local function ellipsis(s,n)
+ return #s > n and (s:sub(1,n-3) .. '...') or s
+end
+
+local align = {}
+
+function align.left(s,n)
+ s = s..(' '):rep(n - #s)
+ return ellipsis(s,n)
+end
+
+function align.right(s,n)
+ s = (' '):rep(n - #s)..s
+ return ellipsis(s,n)
+end
+
+function align.center(s,n)
+ local total = n - #s
+ local left = math.floor(total / 2)
+ local right = math.ceil(total / 2)
+ s = (' '):rep(left)..s..(' '):rep(right)
+ return ellipsis(s,n)
+end
+
+local function fit(s,n,al)
+ return align[al or 'left'](s,n)
+end
+
+local function print_table(fields, rows, aligns, minsize, print)
+ print = print or _G.print
+ minsize = minsize or math.huge
+ local max_sizes = {}
+ for i=1,#rows do
+ for j=1,#fields do
+ max_sizes[j] = math.min(minsize, math.max(max_sizes[j] or 0, #rows[i][j]))
+ end
+ end
+
+ local totalsize = 0
+ for j=1,#fields do
+ max_sizes[j] = math.max(max_sizes[j] or 0, #fields[j])
+ totalsize = totalsize + max_sizes[j] + 3
+ end
+
+ print()
+ local s, ps = '', ''
+ for j=1,#fields do
+ s = s .. fit(fields[j], max_sizes[j], 'center') .. ' | '
+ ps = ps .. ('-'):rep(max_sizes[j]) .. ' + '
+ end
+ print(s)
+ print(ps)
+
+ for i=1,#rows do
+ local s = ''
+ for j=1,#fields do
+ local val = rows[i][j]
+ s = s .. fit(val, max_sizes[j], aligns and aligns[j]) .. ' | '
+ end
+ print(s)
+ end
+ print()
+end
+
+local function invert_table(fields, rows, minsize)
+ local ft, rt = {'field'}, {}
+ for i=1,#rows do
+ ft[i+1] = tostring(i)
+ end
+ for j=1,#fields do
+ local row = {fields[j]}
+ for i=1,#rows do
+ row[i+1] = rows[i][j]
+ end
+ rt[j] = row
+ end
+ return ft, rt
+end
+
+local function format_cell(v)
+ if v == nil then
+ return 'NULL'
+ else
+ return tostring(v)
+ end
+end
+
+local function cell_align(current_align, cell_value)
+ if current_align == 'left' then return 'left' end
+ if type(cell_value) == 'number' or type(cell_value) == 'cdata' then return 'right' end
+ return 'left'
+end
+
+local function print_result(res, minsize, print)
+ local fields = {}
+ for i,field in res:fields() do
+ fields[i] = field.name
+ end
+ local rows = {}
+ local aligns = {} --deduced from values
+ for i,row in res:rows'n' do
+ local t = {}
+ for j=1,#fields do
+ t[j] = format_cell(row[j])
+ aligns[j] = cell_align(aligns[j], row[j])
+ end
+ rows[i] = t
+ end
+ print_table(fields, rows, aligns, minsize, print)
+end
+
+local function print_statement(stmt, minsize, print)
+ local res = stmt:bind_result()
+ local fields = {}
+ for i,field in stmt:fields() do
+ fields[i] = field.name
+ end
+ local rows = {}
+ local aligns = {}
+ while stmt:fetch() do
+ local row = {}
+ for i=1,#fields do
+ local v = res:get(i)
+ row[i] = format_cell(v)
+ aligns[i] = cell_align(aligns[i], v)
+ end
+ rows[#rows+1] = row
+ end
+ stmt:close()
+ print_table(fields, rows, aligns, minsize, print)
+end
+
+if not ... then require'mysql_test' end
+
+return {
+ fit = fit,
+ format_cell = format_cell,
+ table = print_table,
+ result = print_result,
+ statement = print_statement,
+}
+
diff --git a/worldmods/mysql_base/mysql/mysql_test.lua b/worldmods/mysql_base/mysql/mysql_test.lua
new file mode 100644
index 0000000..3f0f496
--- /dev/null
+++ b/worldmods/mysql_base/mysql/mysql_test.lua
@@ -0,0 +1,552 @@
+--mysql test unit (see comments for problems with libmariadb)
+--NOTE: create a database called 'test' first to run these tests!
+local mysql = require'mysql'
+local glue = require'glue'
+local pp = require'pp'
+local myprint = require'mysql_print'
+local ffi = require'ffi'
+
+--helpers
+
+local print_table = myprint.table
+local print_result = myprint.result
+local fit = myprint.fit
+
+local function assert_deepequal(t1, t2) --assert the equality of two values
+ assert(type(t1) == type(t2), type(t1)..' ~= '..type(t2))
+ if type(t1) == 'table' then
+ for k,v in pairs(t1) do assert_deepequal(t2[k], v) end
+ for k,v in pairs(t2) do assert_deepequal(t1[k], v) end
+ else
+ assert(t1 == t2, pp.format(t1) .. ' ~= ' .. pp.format(t2))
+ end
+end
+
+local function print_fields(fields_iter)
+ local fields = {'name', 'type', 'type_flag', 'length', 'max_length', 'decimals', 'charsetnr',
+ 'org_name', 'table', 'org_table', 'db', 'catalog', 'def', 'extension'}
+ local rows = {}
+ local aligns = {}
+ for i,field in fields_iter do
+ rows[i] = {}
+ for j=1,#fields do
+ local v = field[fields[j]]
+ rows[i][j] = tostring(v)
+ aligns[j] = type(v) == 'number' and 'right' or 'left'
+ end
+ end
+ print_table(fields, rows, aligns)
+end
+
+--client library
+
+print('mysql.thread_safe() ', '->', pp.format(mysql.thread_safe()))
+print('mysql.client_info() ', '->', pp.format(mysql.client_info()))
+print('mysql.client_version()', '->', pp.format(mysql.client_version()))
+
+--connections
+
+local t = {
+ host = 'localhost',
+ user = 'root',
+ db = 'test',
+ options = {
+ MYSQL_SECURE_AUTH = false, --not supported by libmariadb
+ MYSQL_OPT_READ_TIMEOUT = 1,
+ },
+ flags = {
+ CLIENT_LONG_PASSWORD = true,
+ },
+}
+local conn = mysql.connect(t)
+print('mysql.connect ', pp.format(t, ' '), '->', conn)
+print('conn:change_user( ', pp.format(t.user), ')', conn:change_user(t.user))
+print('conn:select_db( ', pp.format(t.db), ')', conn:select_db(t.db))
+print('conn:set_multiple_statements(', pp.format(true), ')', conn:set_multiple_statements(true))
+print('conn:set_charset( ', pp.format('utf8'), ')', conn:set_charset('utf8'))
+
+--conn info
+
+print('conn:charset_name() ', '->', pp.format(conn:charset())); assert(conn:charset() == 'utf8')
+print('conn:charset_info() ', '->', pp.format(conn:charset_info(), ' ')) --crashes libmariadb
+print('conn:ping() ', '->', pp.format(conn:ping()))
+print('conn:thread_id() ', '->', pp.format(conn:thread_id()))
+print('conn:stat() ', '->', pp.format(conn:stat()))
+print('conn:server_info() ', '->', pp.format(conn:server_info()))
+print('conn:host_info() ', '->', pp.format(conn:host_info()))
+print('conn:server_version() ', '->', pp.format(conn:server_version()))
+print('conn:proto_info() ', '->', pp.format(conn:proto_info()))
+print('conn:ssl_cipher() ', '->', pp.format(conn:ssl_cipher()))
+
+--transactions
+
+print('conn:commit() ', conn:commit())
+print('conn:rollback() ', conn:rollback())
+print('conn:set_autocommit() ', conn:set_autocommit(true))
+
+--test types and values
+
+local test_fields = {
+ 'fdecimal',
+ 'fnumeric',
+ 'ftinyint',
+ 'futinyint',
+ 'fsmallint',
+ 'fusmallint',
+ 'finteger',
+ 'fuinteger',
+ 'ffloat',
+ 'fdouble',
+ 'fdouble2',
+ 'fdouble3',
+ 'fdouble4',
+ 'freal',
+ 'fbigint',
+ 'fubigint',
+ 'fmediumint',
+ 'fumediumint',
+ 'fdate',
+ 'ftime',
+ 'ftime2',
+ 'fdatetime',
+ 'fdatetime2',
+ 'ftimestamp',
+ 'ftimestamp2',
+ 'fyear',
+ 'fbit2',
+ 'fbit22',
+ 'fbit64',
+ 'fenum',
+ 'fset',
+ 'ftinyblob',
+ 'fmediumblob',
+ 'flongblob',
+ 'ftext',
+ 'fblob',
+ 'fvarchar',
+ 'fvarbinary',
+ 'fchar',
+ 'fbinary',
+ 'fnull',
+}
+
+local field_indices = glue.index(test_fields)
+
+local field_types = {
+ fdecimal = 'decimal(8,2)',
+ fnumeric = 'numeric(6,4)',
+ ftinyint = 'tinyint',
+ futinyint = 'tinyint unsigned',
+ fsmallint = 'smallint',
+ fusmallint = 'smallint unsigned',
+ finteger = 'int',
+ fuinteger = 'int unsigned',
+ ffloat = 'float',
+ fdouble = 'double',
+ fdouble2 = 'double',
+ fdouble3 = 'double',
+ fdouble4 = 'double',
+ freal = 'real',
+ fbigint = 'bigint',
+ fubigint = 'bigint unsigned',
+ fmediumint = 'mediumint',
+ fumediumint = 'mediumint unsigned',
+ fdate = 'date',
+ ftime = 'time(0)',
+ ftime2 = 'time(6)',
+ fdatetime = 'datetime(0)',
+ fdatetime2 = 'datetime(6)',
+ ftimestamp = 'timestamp(0) null',
+ ftimestamp2 = 'timestamp(6) null',
+ fyear = 'year',
+ fbit2 = 'bit(2)',
+ fbit22 = 'bit(22)',
+ fbit64 = 'bit(64)',
+ fenum = "enum('yes', 'no')",
+ fset = "set('e1', 'e2', 'e3')",
+ ftinyblob = 'tinyblob',
+ fmediumblob = 'mediumblob',
+ flongblob = 'longblob',
+ ftext = 'text',
+ fblob = 'blob',
+ fvarchar = 'varchar(200)',
+ fvarbinary = 'varbinary(200)',
+ fchar = 'char(200)',
+ fbinary = 'binary(20)',
+ fnull = 'int'
+}
+
+local test_values = {
+ fdecimal = '42.12',
+ fnumeric = '42.1234',
+ ftinyint = 42,
+ futinyint = 255,
+ fsmallint = 42,
+ fusmallint = 65535,
+ finteger = 42,
+ fuinteger = 2^32-1,
+ ffloat = tonumber(ffi.cast('float', 42.33)),
+ fdouble = 42.33,
+ fdouble2 = nil, --null from mysql 5.1.24+
+ fdouble3 = nil, --null from mysql 5.1.24+
+ fdouble4 = nil, --null from mysql 5.1.24+
+ freal = 42.33,
+ fbigint = 420LL,
+ fubigint = 0ULL - 1,
+ fmediumint = 440,
+ fumediumint = 2^24-1,
+ fdate = {year = 2013, month = 10, day = 05},
+ ftime = {hour = 21, min = 30, sec = 15, frac = 0},
+ ftime2 = {hour = 21, min = 30, sec = 16, frac = 123456},
+ fdatetime = {year = 2013, month = 10, day = 05, hour = 21, min = 30, sec = 17, frac = 0},
+ fdatetime2 = {year = 2013, month = 10, day = 05, hour = 21, min = 30, sec = 18, frac = 123456},
+ ftimestamp = {year = 2013, month = 10, day = 05, hour = 21, min = 30, sec = 19, frac = 0},
+ ftimestamp2 = {year = 2013, month = 10, day = 05, hour = 21, min = 30, sec = 20, frac = 123456},
+ fyear = 2013,
+ fbit2 = 2,
+ fbit22 = 2 * 2^8 + 2,
+ fbit64 = 2ULL * 2^(64-8) + 2 * 2^8 + 2,
+ fenum = 'yes',
+ fset = 'e2,e3',
+ ftinyblob = 'tiny tiny blob',
+ fmediumblob = 'medium blob',
+ flongblob = 'loong blob',
+ ftext = 'just a text',
+ fblob = 'bloob',
+ fvarchar = 'just a varchar',
+ fvarbinary = 'a varbinary',
+ fchar = 'a char',
+ fbinary = 'a binary char\0\0\0\0\0\0\0',
+ fnull = nil,
+}
+
+local set_values = {
+ fdecimal = "'42.12'",
+ fnumeric = "42.1234",
+ ftinyint = "'42'",
+ futinyint = "'255'",
+ fsmallint = "42",
+ fusmallint = "65535",
+ finteger = "'42'",
+ fuinteger = tostring(2^32-1),
+ ffloat = "42.33",
+ fdouble = "'42.33'",
+ fdouble2 = "0/0",
+ fdouble3 = "1/0",
+ fdouble4 = "-1/0",
+ freal = "42.33",
+ fbigint = "'420'",
+ fubigint = tostring(0ULL-1):sub(1,-4), --remove 'ULL'
+ fmediumint = "440",
+ fumediumint = tostring(2^24-1),
+ fdate = "'2013-10-05'",
+ ftime = "'21:30:15'",
+ ftime2 = "'21:30:16.123456'",
+ fdatetime = "'2013-10-05 21:30:17'",
+ fdatetime2 = "'2013-10-05 21:30:18.123456'",
+ ftimestamp = "'2013-10-05 21:30:19'",
+ ftimestamp2 = "'2013-10-05 21:30:20.123456'",
+ fyear = "2013",
+ fbit2 = "b'10'",
+ fbit22 = "b'1000000010'",
+ fbit64 = "b'0000001000000000000000000000000000000000000000000000001000000010'",
+ fenum = "'yes'",
+ fset = "('e3,e2')",
+ ftinyblob = "'tiny tiny blob'",
+ fmediumblob = "'medium blob'",
+ flongblob = "'loong blob'",
+ ftext = "'just a text'",
+ fblob = "'bloob'",
+ fvarchar = "'just a varchar'",
+ fvarbinary = "'a varbinary'",
+ fchar = "'a char'",
+ fbinary = "'a binary char'",
+ fnull = "null"
+}
+
+local bind_types = {
+ fdecimal = 'decimal(20)', --TODO: truncation
+ fnumeric = 'numeric(20)',
+ ftinyint = 'tinyint',
+ futinyint = 'tinyint unsigned',
+ fsmallint = 'smallint',
+ fusmallint = 'smallint unsigned',
+ finteger = 'int',
+ fuinteger = 'int unsigned',
+ ffloat = 'float',
+ fdouble = 'double',
+ fdouble2 = 'double',
+ fdouble3 = 'double',
+ fdouble4 = 'double',
+ freal = 'real',
+ fbigint = 'bigint',
+ fubigint = 'bigint unsigned',
+ fmediumint = 'mediumint',
+ fumediumint = 'mediumint unsigned',
+ fdate = 'date',
+ ftime = 'time',
+ ftime2 = 'time',
+ fdatetime = 'datetime',
+ fdatetime2 = 'datetime',
+ ftimestamp = 'timestamp',
+ ftimestamp2 = 'timestamp',
+ fyear = 'year',
+ fbit2 = 'bit(2)',
+ fbit22 = 'bit(22)',
+ fbit64 = 'bit(64)',
+ fenum = 'enum(200)',
+ fset = 'set(200)',
+ ftinyblob = 'tinyblob(200)',
+ fmediumblob = 'mediumblob(200)',
+ flongblob = 'longblob(200)',
+ ftext = 'text(200)',
+ fblob = 'blob(200)',
+ fvarchar = 'varchar(200)',
+ fvarbinary = 'varbinary(200)',
+ fchar = 'char(200)',
+ fbinary = 'binary(200)',
+ fnull = 'int',
+}
+
+--queries
+
+local esc = "'escape me'"
+print('conn:escape( ', pp.format(esc), ')', '->', pp.format(conn:escape(esc)))
+local q1 = 'drop table if exists binding_test'
+print('conn:query( ', pp.format(q1), ')', conn:query(q1))
+
+local field_defs = ''
+for i,field in ipairs(test_fields) do
+ field_defs = field_defs .. field .. ' ' .. field_types[field] .. (i == #test_fields and '' or ', ')
+end
+
+local field_sets = ''
+for i,field in ipairs(test_fields) do
+ field_sets = field_sets .. field .. ' = ' .. set_values[field] .. (i == #test_fields and '' or ', ')
+end
+
+conn:query([[
+create table binding_test ( ]] .. field_defs .. [[ );
+
+insert into binding_test set ]] .. field_sets .. [[ ;
+
+insert into binding_test values ();
+
+select * from binding_test;
+]])
+
+--query info
+
+print('conn:field_count() ', '->', pp.format(conn:field_count()))
+print('conn:affected_rows() ', '->', pp.format(conn:affected_rows()))
+print('conn:insert_id() ', '->', conn:insert_id())
+print('conn:errno() ', '->', pp.format(conn:errno()))
+print('conn:sqlstate() ', '->', pp.format(conn:sqlstate()))
+print('conn:warning_count() ', '->', pp.format(conn:warning_count()))
+print('conn:info() ', '->', pp.format(conn:info()))
+for i=1,3 do
+print('conn:more_results() ', '->', pp.format(conn:more_results())); assert(conn:more_results())
+print('conn:next_result() ', '->', pp.format(conn:next_result()))
+end
+assert(not conn:more_results())
+
+--query results
+
+local res = conn:store_result() --TODO: local res = conn:use_result()
+print('conn:store_result() ', '->', res)
+print('res:row_count() ', '->', pp.format(res:row_count())); assert(res:row_count() == 2)
+print('res:field_count() ', '->', pp.format(res:field_count())); assert(res:field_count() == #test_fields)
+print('res:eof() ', '->', pp.format(res:eof())); assert(res:eof() == true)
+print('res:fields() ', '->') print_fields(res:fields())
+print('res:field_info(1) ', '->', pp.format(res:field_info(1)))
+
+--first row: fetch as array and test values
+local row = assert(res:fetch'n')
+print("res:fetch'n' ", '->', pp.format(row))
+for i,field in res:fields() do
+ assert_deepequal(row[i], test_values[field.name])
+end
+
+--first row again: fetch as assoc. array and test values
+print('res:seek(1) ', '->', res:seek(1))
+local row = assert(res:fetch'a')
+print("res:fetch'a' ", '->', pp.format(row))
+for i,field in res:fields() do
+ assert_deepequal(row[field.name], test_values[field.name])
+end
+
+--first row again: fetch unpacked and test values
+print('res:seek(1) ', '->', res:seek(1))
+local function pack(_, ...)
+ local t = {}
+ for i=1,select('#', ...) do
+ t[i] = select(i, ...)
+ end
+ return t
+end
+local row = pack(res:fetch())
+print("res:fetch() ", '-> packed: ', pp.format(row))
+for i,field in res:fields() do
+ assert_deepequal(row[i], test_values[field.name])
+end
+
+--first row again: print its values parsed and unparsed for comparison
+res:seek(1)
+local row = assert(res:fetch'n')
+res:seek(1)
+local row_s = assert(res:fetch'ns')
+print()
+print(fit('', 4, 'right') .. ' ' .. fit('field', 20) .. fit('unparsed', 40) .. ' ' .. 'parsed')
+print(('-'):rep(4 + 2 + 20 + 40 + 40))
+for i,field in res:fields() do
+ print(fit(tostring(i), 4, 'right') .. ' ' .. fit(field.name, 20) .. fit(pp.format(row_s[i]), 40) .. ' ' .. pp.format(row[i]))
+end
+print()
+
+--second row: all nulls
+local row = assert(res:fetch'n')
+print("res:fetch'n' ", '->', pp.format(row))
+assert(#row == 0)
+for i=1,res:field_count() do
+ assert(row[i] == nil)
+end
+assert(not res:fetch'n')
+
+--all rows again: test iterator
+res:seek(1)
+local n = 0
+for i,row in res:rows'nas' do
+ n = n + 1
+ assert(i == n)
+end
+print("for i,row in res:rows'nas' do ", '->', n); assert(n == 2)
+
+print('res:free() ', res:free())
+
+--reflection
+
+print('res:list_dbs() ', '->'); print_result(conn:list_dbs())
+print('res:list_tables() ', '->'); print_result(conn:list_tables())
+print('res:list_processes() ', '->'); print_result(conn:list_processes())
+
+--prepared statements
+
+local query = 'select '.. table.concat(test_fields, ', ')..' from binding_test'
+local stmt = conn:prepare(query)
+
+print('conn:prepare( ', pp.format(query), ')', '->', stmt)
+print('stmt:field_count() ', '->', pp.format(stmt:field_count())); assert(stmt:field_count() == #test_fields)
+--we can get the fields and their types before execution so we can create create our bind structures.
+--max. length is not computed though, but length is, so we can use that.
+print('stmt:fields() ', '->'); print_fields(stmt:fields())
+
+--binding phase
+
+local btypes = {}
+for i,field in ipairs(test_fields) do
+ btypes[i] = bind_types[field]
+end
+local bind = stmt:bind_result(btypes)
+print('stmt:bind_result( ', pp.format(btypes), ')', '->', pp.format(bind))
+
+--execution and loading
+
+print('stmt:exec() ', stmt:exec())
+print('stmt:store_result() ', stmt:store_result())
+
+--result info
+
+print('stmt:row_count() ', '->', pp.format(stmt:row_count()))
+print('stmt:affected_rows() ', '->', pp.format(stmt:affected_rows()))
+print('stmt:insert_id() ', '->', pp.format(stmt:insert_id()))
+print('stmt:sqlstate() ', '->', pp.format(stmt:sqlstate()))
+
+--result data (different API since we don't get a result object)
+
+print('stmt:fetch() ', stmt:fetch())
+
+print('stmt:fields() ', '->'); print_fields(stmt:fields())
+
+print('bind:is_truncated(1) ', '->', pp.format(bind:is_truncated(1))); assert(bind:is_truncated(1) == false)
+print('bind:is_null(1) ', '->', pp.format(bind:is_null(1))); assert(bind:is_null(1) == false)
+print('bind:get(1) ', '->', pp.format(bind:get(1))); assert(bind:get(1) == test_values.fdecimal)
+local i = field_indices.fdate
+print('bind:get_date( ', i, ')', '->', bind:get_date(i)); assert_deepequal({bind:get_date(i)}, {2013, 10, 5})
+local i = field_indices.ftime
+print('bind:get_date( ', i, ')', '->', bind:get_date(i)); assert_deepequal({bind:get_date(i)}, {nil, nil, nil, 21, 30, 15, 0})
+local i = field_indices.fdatetime
+print('bind:get_date( ', '->', bind:get_date(i)); assert_deepequal({bind:get_date(i)}, {2013, 10, 5, 21, 30, 17, 0})
+local i = field_indices.ftimestamp
+print('bind:get_date( ', '->', bind:get_date(i)); assert_deepequal({bind:get_date(i)}, {2013, 10, 5, 21, 30, 19, 0})
+local i = field_indices.ftimestamp2
+print('bind:get_date( ', '->', bind:get_date(i)); assert_deepequal({bind:get_date(i)}, {2013, 10, 5, 21, 30, 20, 123456})
+print('for i=1,bind.field_count do bind:get(i)', '->')
+
+local function print_bind_buffer(bind)
+ print()
+ for i,field in ipairs(test_fields) do
+ local v = bind:get(i)
+ assert_deepequal(v, test_values[field])
+ assert(bind:is_truncated(i) == false)
+ assert(bind:is_null(i) == (test_values[field] == nil))
+ print(fit(tostring(i), 4, 'right') .. ' ' .. fit(field, 20) .. pp.format(v))
+ end
+ print()
+end
+print_bind_buffer(bind)
+
+print('stmt:free_result() ', stmt:free_result())
+--local next_result = stmt:next_result()
+--print('stmt:next_result() ', '->', pp.format(next_result)); assert(next_result == false)
+
+print('stmt:reset() ', stmt:reset())
+print('stmt:close() ', stmt:close())
+
+--prepared statements with parameters
+
+for i,field in ipairs(test_fields) do
+ local query = 'select * from binding_test where '..field..' = ?'
+ local stmt = conn:prepare(query)
+ print('conn:prepare( ', pp.format(query), ')')
+ local param_bind_def = {bind_types[field]}
+
+ local bind = stmt:bind_params(param_bind_def)
+ print('stmt:bind_params ', pp.format(param_bind_def))
+
+ local function exec()
+ print('stmt:exec() ', stmt:exec())
+ print('stmt:store_result() ', stmt:store_result())
+ print('stmt:row_count() ', '->', stmt:row_count())
+ assert(stmt:row_count() == 1) --libmariadb() returns 0
+ end
+
+ local v = test_values[field]
+ if v ~= nil then
+ print('bind:set( ', 1, pp.format(v), ')'); bind:set(1, v); exec()
+
+ if field:find'date' or field:find'time' then
+ print('bind:set_date( ', 1, v.year, v.month, v.day, v.hour, v.min, v.sec, v.frac, ')')
+ bind:set_date(1, v.year, v.month, v.day, v.hour, v.min, v.sec, v.frac)
+ exec() --libmariadb crashes the server
+ end
+ end
+ print('stmt:close() ', stmt:close())
+end
+
+--prepared statements with auto-allocated result bind buffers.
+
+local query = 'select * from binding_test'
+local stmt = conn:prepare(query)
+local bind = stmt:bind_result()
+--pp(stmt:bind_result_types())
+stmt:exec()
+stmt:store_result()
+stmt:fetch()
+print_bind_buffer(bind)
+stmt:close()
+
+local q = 'drop table binding_test'
+print('conn:query( ', pp.format(q), ')', conn:query(q))
+print('conn:commit() ', conn:commit())
+print('conn:close() ', conn:close())
+