From 924d0cd38a006792b8911cee3965eab5589d8e1b Mon Sep 17 00:00:00 2001 From: elite Date: Fri, 7 Jul 2017 18:59:22 -0400 Subject: [PATCH] enable mysql mods --- worldmods/mysql_auth/README.md | 123 ++ worldmods/mysql_auth/auth_txt_import.lua | 47 + worldmods/mysql_auth/depends.txt | 2 + worldmods/mysql_auth/init.lua | 299 +++++ worldmods/mysql_base/LICENSE.txt | 165 +++ worldmods/mysql_base/README.md | 108 ++ worldmods/mysql_base/init.lua | 186 +++ worldmods/mysql_base/mysql/mysql.lua | 1303 ++++++++++++++++++++ worldmods/mysql_base/mysql/mysql.md | 548 ++++++++ worldmods/mysql_base/mysql/mysql_h.lua | 571 +++++++++ worldmods/mysql_base/mysql/mysql_print.lua | 144 +++ worldmods/mysql_base/mysql/mysql_test.lua | 552 +++++++++ 12 files changed, 4048 insertions(+) create mode 100644 worldmods/mysql_auth/README.md create mode 100644 worldmods/mysql_auth/auth_txt_import.lua create mode 100644 worldmods/mysql_auth/depends.txt create mode 100644 worldmods/mysql_auth/init.lua create mode 100644 worldmods/mysql_base/LICENSE.txt create mode 100644 worldmods/mysql_base/README.md create mode 100644 worldmods/mysql_base/init.lua create mode 100644 worldmods/mysql_base/mysql/mysql.lua create mode 100644 worldmods/mysql_base/mysql/mysql.md create mode 100644 worldmods/mysql_base/mysql/mysql_h.lua create mode 100644 worldmods/mysql_base/mysql/mysql_print.lua create mode 100644 worldmods/mysql_base/mysql/mysql_test.lua 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()) +