diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a403fd9f3193366ab58726ef580c0482951c56ab..dab1cd806e8c4929c05733a19499ee1300b3b7a2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -113,6 +113,10 @@ if (ENABLE_PSQL) set (common_sources ${common_sources} lua/pg.m) endif() +if (ENABLE_MYSQL) + set (common_sources ${common_sources} lua/mysql.m) +endif() + if (ENABLE_TRACE) set (common_sources ${common_sources} trace.m) endif() diff --git a/src/lua/lua_mysql.h b/src/lua/lua_mysql.h new file mode 100644 index 0000000000000000000000000000000000000000..bd1c383e9a178989d50e959d3ef47f39e4679cff --- /dev/null +++ b/src/lua/lua_mysql.h @@ -0,0 +1,7 @@ +#ifndef TARANTOOL_LUA_SQL_MYSQL_H_INCLUDED + +struct lua_State; +int lbox_net_mysql_connect(struct lua_State *L); + +#endif /* TARANTOOL_LUA_SQL_MYSQL_H_INCLUDED */ + diff --git a/src/lua/lua_sql.m b/src/lua/lua_sql.m index 100ebea7561abdc1417c176472689f5fa8b87327..f55a39dfdd7c15dd1b4a7a906640563f0f5d5298 100644 --- a/src/lua/lua_sql.m +++ b/src/lua/lua_sql.m @@ -43,6 +43,8 @@ lbox_net_sql_do_connect(struct lua_State *L) "Use cmake with '-DENABLE_MYSQL=ON' option." ); #endif + + luaL_error(L, "Unknown driver '%s'", drv); return 0; } diff --git a/src/lua/mysql.m b/src/lua/mysql.m new file mode 100644 index 0000000000000000000000000000000000000000..1db0a736e33c9054c95aa924d47fe6d841e506a7 --- /dev/null +++ b/src/lua/mysql.m @@ -0,0 +1,355 @@ +#include "lua_mysql.h" +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <coeio.h> +#include <tarantool_ev.h> + + +#include <lua/init.h> +#include <say.h> +#include <mysql.h> + + +static MYSQL * +lua_check_mysql(struct lua_State *L, int index) +{ + int pop = 0; + if (lua_istable(L, index)) { + if (index < 0) + index--; + lua_pushstring(L, "raw"); + lua_rawget(L, index); + + pop = 1; + index = -1; + } + + if (!lua_isuserdata(L, index)) + luaL_error(L, "Can't extract userdata from lua-stack"); + + MYSQL *mysql = *(void **)lua_touserdata(L, index); + if (pop) + lua_pop(L, pop); + return mysql; +} + +static ssize_t +connect_mysql(va_list ap) +{ + MYSQL *mysql = va_arg(ap, typeof(mysql)); + const char *host = va_arg(ap, typeof(host)); + const char *port = va_arg(ap, typeof(port)); + const char *user = va_arg(ap, typeof(user)); + const char *password = va_arg(ap, typeof(password)); + const char *db = va_arg(ap, typeof(db)); + + + int iport = 0; + const char *usocket = 0; + + if (strcmp(host, "unix/") == 0) { + usocket = port; + host = NULL; + } else { + iport = atoi(port); + if (iport == 0) + iport = 3306; + } + + say_info("host=%s user=%s password=%s db=%s iport=%d usocket=%s", + host, user, password, db, iport, usocket); + + mysql_real_connect(mysql, host, user, password, db, iport, usocket, + CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); + return 0; +} + + +/** returns self.field as C-string */ +static const char * +self_field(struct lua_State *L, const char *name, int index) +{ + lua_pushstring(L, name); + if (index < 0) + index--; + lua_rawget(L, index); + const char *res = lua_tostring(L, -1); + lua_pop(L, 1); + return res; +} + + +static ssize_t +exec_mysql(va_list ap) +{ + MYSQL *mysql = va_arg(ap, typeof(mysql)); + const char *sql = va_arg(ap, typeof(sql)); + size_t len = va_arg(ap, typeof(len)); + + int res = mysql_real_query(mysql, sql, len); + if (res == 0) { + return 0; + } + return -2; +} + +static ssize_t +fetch_result(va_list ap) +{ + MYSQL *mysql = va_arg(ap, typeof(mysql)); + MYSQL_RES **result = va_arg(ap, typeof(result)); + int resno = va_arg(ap, typeof(resno)); + if (resno) { + if (mysql_next_result(mysql) > 0) + return -2; + } + *result = mysql_store_result(mysql); + return 0; +} + +int +lua_push_mysql_result(struct lua_State *L, MYSQL *mysql, + MYSQL_RES *result, int resno) +{ + if (!result) { + if (mysql_field_count(mysql) == 0) { + lua_newtable(L); + lua_pushnumber(L, mysql_affected_rows(mysql)); + return 2; + } + luaL_error(L, "%s", mysql_error(mysql)); + } + + int tidx; + + if (resno > 0) { + tidx = lua_gettop(L) - 1; + } else { + lua_newtable(L); + tidx = lua_gettop(L); + } + + MYSQL_ROW row; + MYSQL_FIELD * fields = mysql_fetch_fields(result); + while((row = mysql_fetch_row(result))) { + lua_pushnumber(L, luaL_getn(L, tidx) + 1); + lua_newtable(L); + + unsigned long *len = mysql_fetch_lengths(result); + + for (int i = 0; i < mysql_num_fields(result); i++) { + lua_pushstring(L, fields[i].name); + + switch(fields[i].type) { + case MYSQL_TYPE_TINY: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_FLOAT: + case MYSQL_TYPE_INT24: + case MYSQL_TYPE_DOUBLE: { + lua_pushlstring(L, row[i], len[i]); + double v = lua_tonumber(L, -1); + lua_pop(L, 1); + lua_pushnumber(L, v); + break; + } + + case MYSQL_TYPE_NULL: + lua_pushnil(L); + break; + + + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_TIMESTAMP: { + /* hack: lua num64 doesn't work propery + with cjson */ + long long v = atoll(row[i]); + if (v < (1LL << 31)) + lua_pushnumber(L, v); + else + luaL_pushnumber64(L, v); + break; + } + + /* AS string */ + case MYSQL_TYPE_NEWDECIMAL: + case MYSQL_TYPE_DECIMAL: + default: + lua_pushlstring(L, row[i], len[i]); + break; + + } + lua_settable(L, -3); + } + + lua_settable(L, tidx); + } + + if (resno > 0) { + double v = lua_tonumber(L, -1); + v += mysql_affected_rows(mysql); + lua_pop(L, 1); + lua_pushnumber(L, v); + } else { + lua_pushnumber(L, mysql_affected_rows(mysql)); + } + return 2; +} + +int +lua_mysql_execute(struct lua_State *L) +{ + MYSQL *mysql = lua_check_mysql(L, 1); + size_t len; + const char *sql = lua_tolstring(L, 2, &len); + + luaL_Buffer b; + luaL_buffinit(L, &b); + int idx = 3; + + char *q = NULL; + /* process placeholders */ + for (size_t i = 0; i < len; i++) { + if (sql[i] != '?') { + luaL_addchar(&b, sql[i]); + continue; + } + + if (lua_gettop(L) < idx) { + free(q); + luaL_error(L, + "Can't find value for %d placeholder", idx); + } + + if (lua_isboolean(L, idx)) { + int v = lua_toboolean(L, idx++); + luaL_addstring(&b, v ? "TRUE" : "FALSE"); + continue; + } + + if (lua_isnil(L, idx)) { + idx++; + luaL_addstring(&b, "NULL"); + continue; + } + + if (lua_isnumber(L, idx)) { + const char *s = lua_tostring(L, idx++); + luaL_addstring(&b, s); + continue; + } + + size_t l; + const char *s = lua_tolstring(L, idx++, &l); + char *nq = realloc(q, l * 2 + 1); + if (!nq) { + free(q); + luaL_error(L, "Can't allocate memory for variable"); + } + q = nq; + l = mysql_real_escape_string(mysql, q, s, l); + luaL_addchar(&b, '\''); + luaL_addlstring(&b, q, l); + luaL_addchar(&b, '\''); + } + free(q); + + luaL_pushresult(&b); + + sql = lua_tolstring(L, -1, &len); + + + int res = coeio_custom(exec_mysql, TIMEOUT_INFINITY, mysql, sql, len); + if (res == -1) + luaL_error(L, "%s", strerror(errno)); + + if (res != 0) + luaL_error(L, "%s", mysql_error(mysql)); + + int resno = 0; + do { + MYSQL_RES *result = NULL; + res = coeio_custom(fetch_result, TIMEOUT_INFINITY, + mysql, &result, resno); + if (res == -1) + luaL_error(L, "%s", strerror(errno)); + + lua_push_mysql_result(L, mysql, result, resno++); + mysql_free_result(result); + + } while(mysql_more_results(mysql)); + + return 2; +} + + +int +lua_mysql_gc(struct lua_State *L) +{ + MYSQL *mysql = lua_check_mysql(L, 1); + mysql_close(mysql); + return 0; +} + +int +lbox_net_mysql_connect(struct lua_State *L) +{ + MYSQL *mysql = mysql_init(NULL); + if (!mysql) + luaL_error(L, "Can not allocate memory for connector"); + + const char *host = self_field(L, "host", 1); + const char *port = self_field(L, "port", 1); + const char *user = self_field(L, "user", 1); + const char *pass = self_field(L, "password", 1); + const char *db = self_field(L, "db", 1); + + + if (coeio_custom(connect_mysql, TIMEOUT_INFINITY, mysql, + host, port, user, pass, db) == -1) { + mysql_close(mysql); + luaL_error(L, "%s", strerror(errno)); + } + + if (*mysql_error(mysql)) { + const char *estr = mysql_error(mysql); + char *b = alloca(strlen(estr) + 1); + strcpy(b, estr); + mysql_close(mysql); + luaL_error(L, "%s", estr); + } + + lua_pushstring(L, "raw"); + void **ptr = lua_newuserdata(L, sizeof(mysql)); + *ptr = mysql; + + lua_newtable(L); + lua_pushstring(L, "__index"); + + lua_newtable(L); + + static const struct luaL_reg meta [] = { + {"execute", lua_mysql_execute}, + {NULL, NULL} + }; + luaL_register(L, NULL, meta); + lua_settable(L, -3); + + lua_pushstring(L, "__gc"); + lua_pushcfunction(L, lua_mysql_gc); + lua_settable(L, -3); + + + lua_setmetatable(L, -2); + lua_rawset(L, 1); + + /* return self */ + lua_pushvalue(L, 1); + return 1; +} diff --git a/src/lua/pg.m b/src/lua/pg.m index 7f8ba1d06bd4e1a21853a02eebb74cc72278c167..978e64bded4bfbc6e3860e26e423f8d6c1c110a9 100644 --- a/src/lua/pg.m +++ b/src/lua/pg.m @@ -24,7 +24,6 @@ - static PGconn * lua_check_pgconn(struct lua_State *L, int index) { diff --git a/test/box/net_sql.test b/test/box/net_sql.test index d20f0621b0a84750bf4d61af8982d05017cfe0e6..2bb9622ff02be6a23ec5ea758de14a0fc654614f 100644 --- a/test/box/net_sql.test +++ b/test/box/net_sql.test @@ -1,5 +1,6 @@ # encoding: tarantool +# postgresql exec admin "lua dump = function(t) return box.cjson.encode(t) end" exec admin "lua c = box.net.sql.connect('pg', 'localhost', 5432, 'tarantool', 'tarantool', 'tarantool')" exec admin "lua dump({c:execute('SELECT 123::text AS bla, 345')})" @@ -19,3 +20,18 @@ exec admin "lua dump({c:single('SELECT * FROM (VALUES (1,2), (2,3)) t')})" exec admin "lua dump({c:single('SELECT * FROM (VALUES (1,2)) t')})" exec admin "lua dump({c:perform('SELECT * FROM (VALUES (1,2), (2,3)) t')})" exec admin "lua c:execute('SELEC T')" + +exec admin "lua c = box.net.sql.connect('abcd')" + +# mysql +exec admin "lua c = box.net.sql.connect('mysql', 'localhost', 3306, 'tarantool', 'tarantool', 'tarantool')" +exec admin "lua for k, v in pairs(c) do print(k, ': ', type(v)) end" +exec admin "lua c:execute('SEL ECT 1')" +exec admin "lua dump({c:execute('SELECT ? AS bool1, ? AS bool2, ? AS nil, ? AS num, ? AS str', true, false, nil, 123, 'abc')})" + +exec admin "lua dump({c:execute('SELECT * FROM (SELECT ?) t WHERE 1 = 0', 2)})" +exec admin "lua dump({c:execute('CREATE PROCEDURE p1() BEGIN SELECT 1 AS one; SELECT 2 AS two, 3 AS three; END')})" +exec admin "lua dump({c:execute('CALL p1')})" +exec admin "lua dump({c:execute('DROP PROCEDURE p1')})" +exec admin "lua dump({c:execute('SELECT 1 AS one UNION ALL SELECT 2')})" +exec admin "lua dump({c:execute('SELECT 1 AS one UNION ALL SELECT 2; SELECT ? AS two', 'abc')})"