From 8ca4f1cf15d6f19f1ab031a7e5df5fcd93ab74b2 Mon Sep 17 00:00:00 2001
From: "Dmitry E. Oboukhov" <unera@debian.org>
Date: Fri, 7 Jun 2013 01:23:56 +0400
Subject: [PATCH] MySQL driver pre-alpha

---
 src/CMakeLists.txt    |   4 +
 src/lua/lua_mysql.h   |   7 +
 src/lua/lua_sql.m     |   2 +
 src/lua/mysql.m       | 355 ++++++++++++++++++++++++++++++++++++++++++
 src/lua/pg.m          |   1 -
 test/box/net_sql.test |  16 ++
 6 files changed, 384 insertions(+), 1 deletion(-)
 create mode 100644 src/lua/lua_mysql.h
 create mode 100644 src/lua/mysql.m

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a403fd9f31..dab1cd806e 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 0000000000..bd1c383e9a
--- /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 100ebea756..f55a39dfdd 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 0000000000..1db0a736e3
--- /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 7f8ba1d06b..978e64bded 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 d20f0621b0..2bb9622ff0 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')})"
-- 
GitLab