diff --git a/README.markdown b/README.markdown index b1a63e1..16fe237 100644 --- a/README.markdown +++ b/README.markdown @@ -161,6 +161,118 @@ Synopsis } ``` + +The example for using mysql's prepare-statement: + +```nginx + + # you do not need the following line if you are using + # the ngx_openresty bundle: + lua_package_path "/path/to/lua-resty-mysql/lib/?.lua;;"; + + server { + location /test { + content_by_lua_block { + local mysql = require "resty.mysql" + local json = require "cjson" + + local db, err = mysql:new() + if not db then + ngx.say("failed to instantiate mysql: ", err) + return + end + + db:set_timeout(1000) -- 1 sec + + -- or connect to a unix domain socket file listened + -- by a mysql server: + -- local ok, err, errcode, sqlstate = + -- db:connect({ + -- path = "/path/to/mysql.sock", + -- database = "ngx_test", + -- user = "ngx_test", + -- password = "ngx_test" }) + + local ok, err, errcode, sqlstate = db:connect{ + host = "127.0.0.1", + port = 3306, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test", + max_packet_size = 1024 * 1024 } + + if not ok then + ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errcode, sqlstate = + db:query("drop table if exists cats") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(id serial primary key, " + .. "name varchar(5))") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats (name) " + .. "values ('Bob'),(''),(null)") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT id + FROM cats WHERE id = ? OR id = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", json.encode(statement_id)) + + local res, err = db:execute(statement_id, 1, 2) + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", json.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + local ok, err = db:set_keepalive(10000, 100) + if not ok then + ngx.say("failed to set keepalive: ", err) + return + end + + -- or just close the connection right away: + -- local ok, err = db:close() + -- if not ok then + -- ngx.say("failed to close: ", err) + -- return + -- end + } + } + } +``` + [Back to TOC](#table-of-contents) Methods diff --git a/lib/resty/mysql.lua b/lib/resty/mysql.lua index 99a253e..93cc98c 100644 --- a/lib/resty/mysql.lua +++ b/lib/resty/mysql.lua @@ -2,6 +2,7 @@ local bit = require "bit" +local ffi = require("ffi") local sub = string.sub local tcp = ngx.socket.tcp local strbyte = string.byte @@ -18,10 +19,11 @@ local rshift = bit.rshift local tohex = bit.tohex local sha1 = ngx.sha1_bin local concat = table.concat -local unpack = unpack local setmetatable = setmetatable local error = error local tonumber = tonumber +local ffi_new = ffi.new +local C = ffi.C if not ngx.config @@ -32,6 +34,23 @@ then end +ffi.cdef[[ + typedef union { + float value; + } point_float; + + typedef union { + double value; + } point_double; + + void * memcpy(void *restrict dst, const void *restrict src, size_t n); +]] + + +local c_st_float = ffi_new("point_float") +local c_st_double = ffi_new("point_double") + + local ok, new_tab = pcall(require, "table.new") if not ok then new_tab = function (narr, nrec) return {} end @@ -49,9 +68,40 @@ local STATE_COMMAND_SENT = 2 local COM_QUIT = 0x01 local COM_QUERY = 0x03 local CLIENT_SSL = 0x0800 +local COM_STMT_PREPARE = 0x16 +local COM_STMT_EXECUTE = 0x17 local SERVER_MORE_RESULTS_EXISTS = 8 +-- mysql data type +local MYSQL_TYPE_DECIMAL = 0 +local MYSQL_TYPE_TINY = 1 +local MYSQL_TYPE_SHORT = 2 +local MYSQL_TYPE_LONG = 3 +local MYSQL_TYPE_FLOAT = 4 +local MYSQL_TYPE_DOUBLE = 5 +local MYSQL_TYPE_NULL = 6 +local MYSQL_TYPE_TIMESTAMP = 7 +local MYSQL_TYPE_LONGLONG = 8 +local MYSQL_TYPE_INT24 = 9 +local MYSQL_TYPE_DATE = 10 +local MYSQL_TYPE_TIME = 11 +local MYSQL_TYPE_DATETIME = 12 +local MYSQL_TYPE_YEAR = 13 +local MYSQL_TYPE_NEWDATE = 14 +local MYSQL_TYPE_VARCHAR = 15 +local MYSQL_TYPE_BIT = 16 +local MYSQL_TYPE_NEWDECIMAL = 246 +local MYSQL_TYPE_ENUM = 247 +local MYSQL_TYPE_SET = 248 +local MYSQL_TYPE_TINY_BLOB = 249 +local MYSQL_TYPE_MEDIUM_BLOB = 250 +local MYSQL_TYPE_LONG_BLOB = 251 +local MYSQL_TYPE_BLOB = 252 +local MYSQL_TYPE_VAR_STRING = 253 +local MYSQL_TYPE_STRING = 254 +local MYSQL_TYPE_GEOMETRY = 255 + -- 16MB - 1, the default max allowed packet size used by libmysqlclient local FULL_PACKET_SIZE = 16777215 @@ -62,14 +112,20 @@ local mt = { __index = _M } -- mysql field value type converters local converters = new_tab(0, 8) -for i = 0x01, 0x05 do +for i = MYSQL_TYPE_TINY, MYSQL_TYPE_DOUBLE do -- tiny, short, long, float, double converters[i] = tonumber end -- converters[0x08] = tonumber -- long long -converters[0x09] = tonumber -- int24 -converters[0x0d] = tonumber -- year -converters[0xf6] = tonumber -- newdecimal +converters[MYSQL_TYPE_INT24] = tonumber -- int24 +converters[MYSQL_TYPE_YEAR] = tonumber -- year +converters[MYSQL_TYPE_NEWDECIMAL] = tonumber -- newdecimal + + +local function _get_byte1(data, i) + local a = strbyte(data, i) + return a, i + 1 +end local function _get_byte2(data, i) @@ -103,6 +159,19 @@ local function _get_byte8(data, i) end +local function _get_byte_bit(data) + -- get bytes which encoded with bit + local a = strbyte(data, 1) + local len = #data + + for j = 2, len do + a = bor(lshift(a, 8), strbyte(data, j)) + end + + return a +end + + local function _set_byte2(n) return strchar(band(n, 0xff), band(rshift(n, 8), 0xff)) end @@ -153,13 +222,13 @@ local function _dump(data) end -local function _dumphex(data) +local function _dumphex(data, con_str) local len = #data local bytes = new_tab(len, 0) for i = 1, len do bytes[i] = tohex(strbyte(data, i), 2) end - return concat(bytes, " ") + return concat(bytes, con_str or " ") end @@ -486,11 +555,15 @@ function _M.set_timeout(self, timeout) end -function _M.connect(self, opts) +function _M.connect(self, opts, only_record) local sock = self.sock if not sock then return nil, "not initialized" end + self.opts = opts + if only_record then + return true + end local max_packet_size = opts.max_packet_size if not max_packet_size then @@ -738,7 +811,7 @@ function _M.server_ver(self) end -local function send_query(self, query) +local function _send_com_package(self, com_package, packet_type) if self.state ~= STATE_CONNECTED then return nil, "cannot send query in the current context: " .. (self.state or "nil") @@ -751,8 +824,8 @@ local function send_query(self, query) self.packet_no = -1 - local cmd_packet = strchar(COM_QUERY) .. query - local packet_len = 1 + #query + local cmd_packet = strchar(packet_type) .. com_package + local packet_len = 1 + #com_package local bytes, err = _send_packet(self, cmd_packet, packet_len) if not bytes then @@ -761,14 +834,216 @@ local function send_query(self, query) self.state = STATE_COMMAND_SENT - --print("packet sent ", bytes, " bytes") + --print("package sent ", bytes, " bytes") return bytes end -_M.send_query = send_query -local function read_result(self, est_nrows) +function _M.send_query(self, query) + local bytes, err = _send_com_package(self, query, COM_QUERY) + return bytes, err +end + + +local function _read_row_length_code(self, est_nrows, cols) + local compact = self.compact + local packet, typ, err + + local rows = new_tab(est_nrows or 4, 0) + local i = 0 + while true do + --print("reading a row") + + packet, typ, err = _recv_packet(self) + if not packet then + return nil, err + end + + if typ == 'EOF' then + local _, status_flags = _parse_eof_packet(packet) + + --print("status flags: ", status_flags) + + if band(status_flags, SERVER_MORE_RESULTS_EXISTS) ~= 0 then + return rows, "again" + end + + break + end + + -- if typ ~= 'DATA' then + -- return nil, 'bad row packet type: ' .. typ + -- end + + -- typ == 'DATA' + + local row = _parse_row_data_packet(packet, cols, compact) + i = i + 1 + rows[i] = row + end + + self.state = STATE_CONNECTED + + return rows +end + + +local function _parse_datetime(str, typ) + local pos = 1 + local year, month, day, hour, minute, second + + if typ == MYSQL_TYPE_DATETIME or + typ == MYSQL_TYPE_TIMESTAMP then + year, pos = _get_byte2(str, pos) + month, pos = _get_byte1(str, pos) + day, pos = _get_byte1(str, pos) + hour, pos = _get_byte1(str, pos) + minute, pos= _get_byte1(str, pos) + second = _get_byte1(str, pos) + + return format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, + hour, minute, second) + + elseif typ == MYSQL_TYPE_DATE then + year, pos = _get_byte2(str, pos) + month, pos = _get_byte1(str, pos) + day = _get_byte1(str, pos) + + return format("%04d-%02d-%02d", year, month, day) + + elseif typ == MYSQL_TYPE_TIME then + pos = 6 + hour, pos = _get_byte1(str, pos) + minute, pos = _get_byte1(str, pos) + second = _get_byte1(str, pos) + + return format("%02d:%02d:%02d", hour, minute, second) + + elseif typ == MYSQL_TYPE_YEAR then + year = _get_byte2(str, pos) + return year + end + + return nil, "unknow date time type:" .. typ +end + + +local function _parse_result_data_packet(data, pos, cols, compact) + local ncols = #cols + local row + if compact then + row = new_tab(ncols, 0) + + else + row = new_tab(0, ncols) + end + + for i = 1, ncols do + local col = cols[i] + local value + + local typ = col.type + local name = col.name + + if typ == MYSQL_TYPE_TINY then + value, pos = _get_byte1(data, pos) + + elseif typ == MYSQL_TYPE_SHORT or + typ == MYSQL_TYPE_YEAR then + value, pos = _get_byte2(data, pos) + + elseif typ == MYSQL_TYPE_LONG then + value, pos = _get_byte4(data, pos) + + elseif typ == MYSQL_TYPE_LONGLONG then + value, pos = _get_byte8(data, pos) + + elseif typ == MYSQL_TYPE_FLOAT then + value = data:sub(pos, pos + 3) + pos = pos + 4 + + C.memcpy(c_st_float, value, 4) + value = c_st_float.value + + elseif typ == MYSQL_TYPE_DOUBLE then + value = data:sub(pos, pos + 7) + pos = pos + 8 + + C.memcpy(c_st_double, value, 8) + value = c_st_double.value + + elseif typ == MYSQL_TYPE_BIT then + value, pos = _from_length_coded_str(data, pos) + value = _get_byte_bit(value) + + elseif typ == MYSQL_TYPE_DATETIME or + typ == MYSQL_TYPE_DATE or + typ == MYSQL_TYPE_TIMESTAMP or + typ == MYSQL_TYPE_TIME then + value, pos = _from_length_coded_str(data, pos) + value = _parse_datetime(value, typ) + + else + value, pos = _from_length_coded_str(data, pos) + end + + -- print("row [", name, "] value: ", value, ", type: ", typ) + + if compact then + row[i] = value + + else + row[name] = value + end + end + + return row +end + + +local function _read_row_bin_type(self, est_nrows, cols) + local compact = self.compact + local field_count = #cols + + local rows = new_tab(est_nrows or 4, 0) + local i = 0 + while true do + local packet, typ, err = _recv_packet(self) + if not packet then + return nil, err + end + + if typ == 'EOF' then + local _, status_flags = _parse_eof_packet(packet) + --print("status flags: ", status_flags) + + if band(status_flags, SERVER_MORE_RESULTS_EXISTS) ~= 0 then + return rows, "again" + end + + break + end + + if typ ~= 'OK' then + return nil, "cannot fetch rows with unexpected packet:" .. typ + end + + local pos = 1 + math.floor((field_count + 9) / 8) + 1 + -- print("_parse_result_data_packet: ", _dumphex(packet:sub(pos))) + + local row = _parse_result_data_packet(packet, pos, cols, compact) + i = i + 1 + rows[i] = row + end + + self.state = STATE_CONNECTED + + return rows +end + + +local function _read_result(self, est_nrows, packet_type) if self.state ~= STATE_COMMAND_SENT then return nil, "cannot read result in the current context: " .. (self.state or "nil") @@ -837,55 +1112,236 @@ local function read_result(self, est_nrows) -- typ == 'EOF' - local compact = self.compact + local rows + if packet_type == COM_QUERY then + rows, err = _read_row_length_code(self, est_nrows, cols) - local rows = new_tab(est_nrows or 4, 0) - local i = 0 - while true do - --print("reading a row") + elseif packet_type == COM_STMT_EXECUTE then + rows, err = _read_row_bin_type(self, est_nrows, cols) + + else + return nil, "unexpected packet type " .. packet_type .. " for " + .. "the input parameters" + end + return rows, err +end + + +local function read_result(self, est_nrows) + return _read_result(self, est_nrows, COM_QUERY) +end +_M.read_result = read_result + + +function _M.query(self, query, est_nrows) + local bytes, err = _send_com_package(self, query, COM_QUERY) + if not bytes then + return nil, "failed to send query: " .. err + end + + return read_result(self, est_nrows) +end + + +local function _read_prepare_init(self) + local packet, typ, err = _recv_packet(self) + if not packet then + return nil, err + end + + if typ == "ERR" then + local errno, msg, sqlstate = _parse_err_packet(packet) + return nil, msg, errno, sqlstate + end + + if typ ~= 'OK' then + return nil, "bad read prepare init packet type: " .. typ + end + + -- typ == 'OK' + local stmt = new_tab(0, 5) + local pos + stmt.field_count, pos = _get_byte1(packet, 1) + stmt.statement_id, pos= _get_byte4(packet, pos) + stmt.columns, pos = _get_byte2(packet, pos) + stmt.parameters, pos = _get_byte2(packet, pos) + if #packet >= 12 then + pos = pos + 1 + stmt.warnings, pos = _get_byte2(packet, pos) + end + + return stmt +end + + +local function _read_prepare_parameters(self, stmt) + local para_count = stmt.parameters + for _ = 1, para_count do + local packet, typ, err = _recv_packet(self) + if not packet then + return nil, err + end + + if typ ~= 'DATA' then + return nil, "bad prepare parameters response type: " .. typ + end + end + + return true +end + + +local function _read_eof_packet(self) + local packet, typ, err = _recv_packet(self) + if not packet then + return nil, err + end + + if typ ~= 'EOF' then + return "unexpected packet type " .. typ .. " while eof packet is " + .. "expected" + end + + return true +end + + +local function _read_result_set(self, field_count) + local result_set = new_tab(0, 2) + local packet, typ, err + + result_set.field_count = field_count + result_set.fields = new_tab(field_count, 0) + + for i = 1, field_count do packet, typ, err = _recv_packet(self) if not packet then return nil, err end - if typ == 'EOF' then - local warning_count, status_flags = _parse_eof_packet(packet) + if typ ~= 'DATA' then + return nil, "_readresult_set type: " .. typ + end - --print("status flags: ", status_flags) + result_set.fields[i] = _parse_field_packet(packet) + end - if band(status_flags, SERVER_MORE_RESULTS_EXISTS) ~= 0 then - return rows, "again" - end + return result_set +end - break + +local function _read_prepare_reponse(self) + if self.state ~= STATE_COMMAND_SENT then + return nil, "cannot read result in the current context: " + .. (self.state or "nil") + end + + local ok + local stmt, err = _read_prepare_init(self) + if err then + self.state = STATE_CONNECTED + return nil, err + end + + if stmt.parameters > 0 then + ok, err = _read_prepare_parameters(self, stmt) + if not ok then + self.state = STATE_CONNECTED + return nil, err end - -- if typ ~= 'DATA' then - -- return nil, 'bad row packet type: ' .. typ - -- end + ok, err = _read_eof_packet(self) + if not ok then + self.state = STATE_CONNECTED + return nil, err + end + end - -- typ == 'DATA' + if stmt.columns > 0 then + stmt.result_set, err = _read_result_set(self, stmt.columns) + if err then + self.state = STATE_CONNECTED + return nil, err + end - local row = _parse_row_data_packet(packet, cols, compact) - i = i + 1 - rows[i] = row + ok, err = _read_eof_packet(self) + if not ok then + self.state = STATE_CONNECTED + return nil, err + end end self.state = STATE_CONNECTED - return rows + return stmt.statement_id, err end -_M.read_result = read_result -function _M.query(self, query, est_nrows) - local bytes, err = send_query(self, query) - if not bytes then - return nil, "failed to send query: " .. err +function _M.prepare(self, sql) + local _, err = _send_com_package(self, sql, COM_STMT_PREPARE) + if err then + return nil, err end - return read_result(self, est_nrows) + local statement + statement, err = _read_prepare_reponse(self) + + return statement, err +end + + +local function _encode_param_types(args) + local buf = new_tab(#args, 0) + + for i, _ in ipairs(args) do + buf[i] = _set_byte2(MYSQL_TYPE_STRING) + end + + return concat(buf, "") +end + + +local function _encode_param_values(args) + local buf = new_tab(#args, 0) + + for i, v in ipairs(args) do + buf[i] = _to_binary_coded_string(tostring(v)) + end + + return concat(buf, "") +end + + +function _M.execute(self, statement_id, ...) + local args = {...} + local type_parm = _encode_param_types(args) + local value_parm = _encode_param_values(args) + + local packet = new_tab(8, 0) + packet[1] = _set_byte4(statement_id) + packet[2] = strchar(0) -- flag + packet[3] = _set_byte4(1) -- iteration-count + + local bitmap_len = (#args + 7) / 8 + local i + for j = 4, 3 + bitmap_len do + -- NULL-bitmap, length: (num-params + 7)/8 + packet[j] = strchar(0) + i = j + end + packet[i + 1] = strchar(1) + packet[i + 2] = type_parm + packet[i + 3] = value_parm + packet = concat(packet, "") + -- print("execute pkg: ", _dumphex(packet)) + + local _, err = _send_com_package(self, packet, COM_STMT_EXECUTE) + if err ~= nil then + return nil, err + end + + return _read_result(self, nil, COM_STMT_EXECUTE) end @@ -894,4 +1350,79 @@ function _M.set_compact_arrays(self, value) end +local function _shallow_copy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in pairs(orig) do + copy[orig_key] = orig_value + end + + else -- number, string, boolean, etc + copy = orig + end + return copy +end + + +function _M.run(self, prepare_sql, ...) + local opts = _shallow_copy(self.opts) + local db, err, res, errcode, sqlstate, used_times, _ + + db, err = _M:new() + if not db then + return nil, err + end + + local database = opts.database or "" + local user = opts.user or "" + local host = opts.host + local pool = user .. ":" .. database .. ":" + if host then + local port = opts.port or 3306 + pool = pool .. host .. ":" .. port .. ":" .. prepare_sql + + else + local path = opts.path + if not path then + return nil, 'neither "host" nor "path" options are specified' + end + + pool = pool .. path .. ":" .. prepare_sql + end + opts.pool = pool + + ok, err, errcode, sqlstate = db:connect(opts) + if not ok then + return nil, "failed to connect: " .. err .. ": " .. errcode + .. " " .. sqlstate + end + + used_times, err = db:get_reused_times() + if err then + return nil, err + end + + if used_times == 0 then + _, err = db:prepare(prepare_sql) + if err then + return nil, err + end + end + + -- print("prepare success: ", json.encode(stmt)) + + -- the statement id shoulds be 1 if there is only one prepare-statement + res, err = db:execute(1, ...) + if err then + return nil, err + end + + db:set_keepalive(1000 * 60 * 5, 10) + + return res +end + + return _M diff --git a/t/prepare.t b/t/prepare.t new file mode 100644 index 0000000..4cf2233 --- /dev/null +++ b/t/prepare.t @@ -0,0 +1,587 @@ +# vim:set ft= ts=4 sw=4 et: + +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +repeat_each(2); + +plan tests => repeat_each() * (3 * blocks()); + +my $pwd = cwd(); + +our $HttpConfig = qq{ + resolver \$TEST_NGINX_RESOLVER; + lua_package_path "$pwd/lib/?.lua;$pwd/t/lib/?.lua;;"; + lua_package_cpath "/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;"; +}; + +$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; +$ENV{TEST_NGINX_MYSQL_PORT} ||= 3306; +$ENV{TEST_NGINX_MYSQL_HOST} ||= '127.0.0.1'; +$ENV{TEST_NGINX_MYSQL_PATH} ||= '/var/run/mysql/mysql.sock'; + +#log_level 'warn'; + +no_long_string(); +no_shuffle(); +check_accum_error_log(); + +run_tests(); + +__DATA__ + +=== TEST 1: prepare +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local mysql = require "resty.mysql" + local ljson = require "ljson" + local db = mysql:new() + + db:set_timeout(1000) -- 1 sec + + local ok, err, errno, sqlstate = db:connect({ + host = "$TEST_NGINX_MYSQL_HOST", + port = $TEST_NGINX_MYSQL_PORT, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test"}) + + if not ok then + ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errno, sqlstate = db:query("drop table if exists cats") + if not res then + ngx.say("bad result: ", err, ": ", errno, ": ", sqlstate, ".") + return + end + + ngx.say("table cats dropped.") + + local res, err, errcode, sqlstate = + db:query("drop table if exists cats") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(id serial primary key, " + .. "name varchar(5))") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats (name) " + .. "values ('Bob'),(''),(null)") + if not res then + ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT id + FROM cats WHERE id = ? OR id = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", statement_id) + + local res, err = db:execute(statement_id, 1, 2) + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", ljson.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + -- local ok, err = db:set_keepalive(10000, 100) + -- if not ok then + -- ngx.say("failed to set keepalive: ", err) + -- return + -- end + + -- or just close the connection right away: + local ok, err = db:close() + if not ok then + ngx.say("failed to close: ", err) + return + end + } + } +--- request +GET /t +--- response_body +connected to mysql. +table cats dropped. +table cats created. +3 rows inserted into table cats (last insert id: 1) +prepare success:1 +execute success:[{"id":1},{"id":2}] +--- no_error_log +[error] + + +=== TEST 2: prepare number type +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local mysql = require "resty.mysql" + local ljson = require "ljson" + local db = mysql:new() + + db:set_timeout(1000) -- 1 sec + + local ok, err, errno, sqlstate = db:connect({ + host = "$TEST_NGINX_MYSQL_HOST", + port = $TEST_NGINX_MYSQL_PORT, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test"}) + + if not ok then + ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errno, sqlstate = db:query("drop table if exists cats") + if not res then + ngx.say("drop table bad result: ", err, ": ", errno, ": ", sqlstate, ".") + return + end + + ngx.say("table cats dropped.") + + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(u_tiny TINYINT, " + .. "u_bit BIT(64)," + .. "bool BOOL," + .. "u_small SMALLINT," -- 4 + .. "u_int INT," + .. "u_integer INTEGER," + .. "u_bigint BIGINT," + .. "u_float FLOAT," -- 8 + .. "u_double DOUBLE," + .. "u_real REAL," + .. "u_decimal DECIMAL," + .. "u_dec DEC," -- 12 + .. "u_num NUMERIC)") + if not res then + ngx.say("create table bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats " + .. "values (1, 2, true, 4, 5, 6, 7, 8.125, 9.23231, 10, 11, 12, 13)") + if not res then + ngx.say("insert values bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT * + FROM cats WHERE u_tiny = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", statement_id) + + local res, err = db:execute(statement_id, 1) + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", ljson.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + -- local ok, err = db:set_keepalive(10000, 100) + -- if not ok then + -- ngx.say("failed to set keepalive: ", err) + -- return + -- end + + -- or just close the connection right away: + local ok, err = db:close() + if not ok then + ngx.say("failed to close: ", err) + return + end + } + } +--- request +GET /t +--- response_body +connected to mysql. +table cats dropped. +table cats created. +1 rows inserted into table cats (last insert id: 0) +prepare success:1 +execute success:[{"bool":1,"u_bigint":7,"u_bit":2,"u_dec":"12","u_decimal":"11","u_double":9.23231,"u_float":8.125,"u_int":5,"u_integer":6,"u_num":"13","u_real":10,"u_small":4,"u_tiny":1}] +--- no_error_log +[error] + + + + +=== TEST 3: prepare string type except ENUM, SET +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local mysql = require "resty.mysql" + local ljson = require "ljson" + local db = mysql:new() + + db:set_timeout(1000) -- 1 sec + + local ok, err, errno, sqlstate = db:connect({ + host = "$TEST_NGINX_MYSQL_HOST", + port = $TEST_NGINX_MYSQL_PORT, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test"}) + + if not ok then + ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errno, sqlstate = db:query("drop table if exists cats") + if not res then + ngx.say("drop table bad result: ", err, ": ", errno, ": ", sqlstate, ".") + return + end + + ngx.say("table cats dropped.") + + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(u_char CHAR, " + .. "u_varchar VARCHAR(12)," + .. "u_tinyblob TINYBLOB," + .. "u_tinytext TINYTEXT," -- 4 + .. "u_blob BLOB," + .. "u_text TEXT," + .. "u_mblob MEDIUMBLOB," + .. "u_mtext MEDIUMTEXT," -- 8 + .. "u_lblob LONGBLOB," + .. "u_ltext LONGTEXT" + .. ")") + if not res then + ngx.say("create table bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats " + .. "values ('c', 'varchar', 'tinyblob', 'u_tinytext', " + .. " 'u_blob', 'u_text', 'u_mblob', 'u_mtext', 'u_lblob'," + .. " 'u_ltext')") + if not res then + ngx.say("insert values bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT * + FROM cats WHERE u_char = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", statement_id) + + local res, err = db:execute(statement_id, 'c') + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", ljson.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + -- local ok, err = db:set_keepalive(10000, 100) + -- if not ok then + -- ngx.say("failed to set keepalive: ", err) + -- return + -- end + + -- or just close the connection right away: + local ok, err = db:close() + if not ok then + ngx.say("failed to close: ", err) + return + end + } + } +--- request +GET /t +--- response_body +connected to mysql. +table cats dropped. +table cats created. +1 rows inserted into table cats (last insert id: 0) +prepare success:1 +execute success:[{"u_blob":"u_blob","u_char":"c","u_lblob":"u_lblob","u_ltext":"u_ltext","u_mblob":"u_mblob","u_mtext":"u_mtext","u_text":"u_text","u_tinyblob":"tinyblob","u_tinytext":"u_tinytext","u_varchar":"varchar"}] +--- no_error_log +[error] + + + +=== TEST 4: prepare type: ENUM, SET +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local mysql = require "resty.mysql" + local ljson = require "ljson" + local db = mysql:new() + + db:set_timeout(1000) -- 1 sec + + local ok, err, errno, sqlstate = db:connect({ + host = "$TEST_NGINX_MYSQL_HOST", + port = $TEST_NGINX_MYSQL_PORT, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test"}) + + if not ok then + ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errno, sqlstate = db:query("drop table if exists cats") + if not res then + ngx.say("drop table bad result: ", err, ": ", errno, ": ", sqlstate, ".") + return + end + + ngx.say("table cats dropped.") + + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(id integer, " + .. "u_enum ENUM('e_1','e_2','e_3')," + .. "u_set SET('a', 'b', 'c')" + .. ")") + if not res then + ngx.say("create table bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats " + .. "values (1, " + .. "2, " + .. " ('a,c'))") + if not res then + ngx.say("insert values bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT * + FROM cats WHERE id = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", statement_id) + + local res, err = db:execute(statement_id, 1) + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", ljson.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + -- local ok, err = db:set_keepalive(10000, 100) + -- if not ok then + -- ngx.say("failed to set keepalive: ", err) + -- return + -- end + + -- or just close the connection right away: + local ok, err = db:close() + if not ok then + ngx.say("failed to close: ", err) + return + end + } + } +--- request +GET /t +--- response_body +connected to mysql. +table cats dropped. +table cats created. +1 rows inserted into table cats (last insert id: 0) +prepare success:1 +execute success:[{"id":1,"u_enum":"e_2","u_set":"a,c"}] +--- no_error_log +[error] + + + +=== TEST 5: prepare type: datetime +--- http_config eval: $::HttpConfig +--- config + location /t { + content_by_lua_block { + local mysql = require "resty.mysql" + local ljson = require "ljson" + local db = mysql:new() + + db:set_timeout(1000) -- 1 sec + + local ok, err, errno, sqlstate = db:connect({ + host = "$TEST_NGINX_MYSQL_HOST", + port = $TEST_NGINX_MYSQL_PORT, + database = "ngx_test", + user = "ngx_test", + password = "ngx_test"}) + + if not ok then + ngx.say("failed to connect: ", err, ": ", errno, " ", sqlstate) + return + end + + ngx.say("connected to mysql.") + + local res, err, errno, sqlstate = db:query("drop table if exists cats") + if not res then + ngx.say("drop table bad result: ", err, ": ", errno, ": ", sqlstate, ".") + return + end + + ngx.say("table cats dropped.") + + + res, err, errcode, sqlstate = + db:query("create table cats " + .. "(id integer, " + .. "u_dt DATETIME," + .. "u_d DATE," + .. "u_ts TIMESTAMP," + .. "u_t TIME," + .. "u_y YEAR" + .. ")") + if not res then + ngx.say("create table bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say("table cats created.") + + res, err, errcode, sqlstate = + db:query("insert into cats " + .. "values (1, " + .. "'2016-08-23 07:09:51', " + .. "'2016-08-23 07:09:51', " + .. "'2016-08-23 07:09:51', " + .. "'2016-08-23 07:09:51', " + .. "'2016')") + if not res then + ngx.say("insert values bad result: ", err, ": ", errcode, ": ", sqlstate, ".") + return + end + + ngx.say(res.affected_rows, " rows inserted into table cats ", + "(last insert id: ", res.insert_id, ")") + + local statement_id, err = db:prepare([[SELECT * + FROM cats WHERE id = ?]]) + if err then + ngx.say("prepare failed:", err) + return + end + + ngx.say("prepare success:", statement_id) + + local res, err = db:execute(statement_id, 1) + if err then + ngx.say("execute failed.", err) + return + end + + ngx.say("execute success:", ljson.encode(res)) + + -- put it into the connection pool of size 100, + -- with 10 seconds max idle timeout + -- local ok, err = db:set_keepalive(10000, 100) + -- if not ok then + -- ngx.say("failed to set keepalive: ", err) + -- return + -- end + + -- or just close the connection right away: + local ok, err = db:close() + if not ok then + ngx.say("failed to close: ", err) + return + end + } + } +--- request +GET /t +--- response_body +connected to mysql. +table cats dropped. +table cats created. +1 rows inserted into table cats (last insert id: 0) +prepare success:1 +execute success:[{"id":1,"u_d":"2016-08-23","u_dt":"2016-08-23 07:09:51","u_t":"07:09:51","u_ts":"2016-08-23 07:09:51","u_y":2016}] +--- no_error_log +[error] +