From 7b271baae19db1528fbe6621bdf50af89a5a336b Mon Sep 17 00:00:00 2001 From: Hans Hagen Date: Fri, 22 Feb 2019 20:29:46 +0100 Subject: 2019-02-22 19:43:00 --- tex/context/base/mkiv/util-sql-imp-ffi.lua | 590 ++++++++++++++++------------- 1 file changed, 321 insertions(+), 269 deletions(-) (limited to 'tex/context/base/mkiv/util-sql-imp-ffi.lua') diff --git a/tex/context/base/mkiv/util-sql-imp-ffi.lua b/tex/context/base/mkiv/util-sql-imp-ffi.lua index 2a2bc6569..3731933f1 100644 --- a/tex/context/base/mkiv/util-sql-imp-ffi.lua +++ b/tex/context/base/mkiv/util-sql-imp-ffi.lua @@ -8,7 +8,7 @@ if not modules then modules = { } end modules ['util-sql-imp-ffi'] = { -- I looked at luajit-mysql to see how the ffi mapping was done but it didn't work -- out that well (at least not on windows) but I got the picture. As I have somewhat --- different demands I simplified / redid the ffi bti and just took the swiglib +-- different demands I simplified / redid the ffi bit and just took the swiglib -- variant and adapted that. local tonumber = tonumber @@ -17,6 +17,7 @@ local format, byte = string.format, string.byte local lpegmatch = lpeg.match local setmetatable, type = setmetatable, type local sleep = os.sleep +local formatters = string.formatters local trace_sql = false trackers.register("sql.trace", function(v) trace_sql = v end) local trace_queries = false trackers.register("sql.queries",function(v) trace_queries = v end) @@ -33,6 +34,9 @@ ffi.cdef [[ a query. The rest is handled already in the Lua code elsewhere. */ + void free(void*ptr); + void * malloc(size_t size); + typedef void MYSQL_instance; typedef void MYSQL_result; typedef char **MYSQL_row; @@ -115,6 +119,14 @@ ffi.cdef [[ MYSQL_result *result ); + unsigned int mysql_affected_rows ( + MYSQL_instance *mysql + ); + + unsigned int mysql_field_count ( + MYSQL_instance *mysql + ); + unsigned int mysql_num_fields ( MYSQL_result *res ); @@ -163,12 +175,14 @@ local dataprepared = helpers.preparetemplate local serialize = sql.serialize local deserialize = sql.deserialize -local mysql_initialize = mysql.mysql_init +local mysql_open_session = mysql.mysql_init local mysql_open_connection = mysql.mysql_real_connect local mysql_execute_query = mysql.mysql_real_query local mysql_close_connection = mysql.mysql_close +local mysql_affected_rows = mysql.mysql_affected_rows +local mysql_field_count = mysql.mysql_field_count local mysql_field_seek = mysql.mysql_field_seek local mysql_num_fields = mysql.mysql_num_fields local mysql_fetch_fields = mysql.mysql_fetch_fields @@ -180,6 +194,7 @@ local mysql_init = mysql.mysql_init local mysql_store_result = mysql.mysql_store_result local mysql_free_result = mysql.mysql_free_result +local mysql_error_number = mysql.mysql_errno local mysql_error_message = mysql.mysql_error local NULL = ffi.cast("MYSQL_result *",0) @@ -187,325 +202,361 @@ local NULL = ffi.cast("MYSQL_result *",0) local ffi_tostring = ffi.string local ffi_gc = ffi.gc ------ mysqldata = ffi.cast("MYSQL_instance*",mysql.malloc(1024*1024)) -local instance = mysql.mysql_init(nil) -- (mysqldata) +local instance = mysql.mysql_init(nil) local mysql_constant_false = false local mysql_constant_true = true -local function finish(t) - local r = t._result_ - if r then - ffi_gc(r,mysql_free_result) +local wrapresult do + + local function collect(t) + local result = t._result_ + if result then + ffi_gc(result,mysql_free_result) + end end -end -local function getcolnames(t) - return t.names -end + local function finish(t) + local result = t._result_ + if result then + t._result_ = nil + ffi_gc(result,mysql_free_result) + end + end -local function getcoltypes(t) - return t.types -end + local function getcoldata(t) + local result = t._result_ + local nofrows = t.nofrows + local noffields = t.noffields + local names = { } + local types = { } + local fields = mysql_fetch_fields(result) + for i=1,noffields do + local field = fields[i-1] + names[i] = ffi_tostring(field.name) + types[i] = tonumber(field.type) -- todo + end + t.names = names + t.types = types + end -local function numrows(t) - return tonumber(t.nofrows) -end + local function getcolnames(t) + local names = t.names + if names then + return names + end + getcoldata(t) + return t.names + end -local function list(t) - local result = t._result_ - if result then - local row = mysql_fetch_row(result) - -- local len = mysql_fetch_lengths(result) - local result = { } - for i=1,t.noffields do - result[i] = ffi_tostring(row[i-1]) + local function getcoltypes(t) + local types = t.types + if types then + return types end - return result + getcoldata(t) + return t.types end -end -local function hash(t) - local result = t._result_ - local fields = t.names - if result then - local row = mysql_fetch_row(result) - -- local len = mysql_fetch_lengths(result) - local result = { } - for i=1,t.noffields do - result[fields[i]] = ffi_tostring(row[i-1]) + local function numrows(t) + return t.nofrows + end + + -- local function fetch(t) + -- local + -- local row = mysql_fetch_row(result) + -- local result = { } + -- for i=1,t.noffields do + -- result[i] = ffi_tostring(row[i-1]) + -- end + -- return unpack(result) + -- end + + local mt = { + __gc = collect, + __index = { + _result_ = nil, + close = finish, + numrows = numrows, + getcolnames = getcolnames, + getcoltypes = getcoltypes, + -- fetch = fetch, -- not efficient + } + } + + wrapresult = function(connection) + local result = mysql_store_result(connection) + if result ~= NULL then + mysql_field_seek(result,0) + local t = { + _result_ = result, + nofrows = tonumber(mysql_num_rows (result) or 0) or 0, + noffields = tonumber(mysql_num_fields(result) or 0) or 0, + } + return setmetatable(t,mt) + elseif tonumber(mysql_field_count(connection) or 0) or 0 > 0 then + return tonumber(mysql_affected_rows(connection)) end - return result end -end -local function wholelist(t) - return fetch_all_rows(t._result_) end -local mt = { __index = { - -- regular - finish = finish, - list = list, - hash = hash, - wholelist = wholelist, - -- compatibility - numrows = numrows, - getcolnames = getcolnames, - getcoltypes = getcoltypes, - -- fallback - _result_ = nil, - names = { }, - types = { }, - noffields = 0, - nofrows = 0, - } -} +local initializesession do -local nt = setmetatable({},mt) + -- timeouts = [ connect_timeout |wait_timeout | interactive_timeout ] --- session + local timeout -- = 3600 -- to be tested -local function close(t) - mysql_close_connection(t._connection_) -end + -- connection -local function execute(t,query) - if query and query ~= "" then - local connection = t._connection_ - local result = mysql_execute_query(connection,query,#query) - if result == 0 then - local result = mysql_store_result(connection) - if result ~= NULL then - mysql_field_seek(result,0) - local nofrows = tonumber(mysql_num_rows(result) or 0) - local noffields = tonumber(mysql_num_fields(result)) - local names = { } - local types = { } - local fields = mysql_fetch_fields(result) - for i=1,noffields do - local field = fields[i-1] - names[i] = ffi_tostring(field.name) - types[i] = tonumber(field.type) -- todo - end - local t = { - _result_ = result, - names = names, - types = types, - noffields = noffields, - nofrows = nofrows, - } - return setmetatable(t,mt) + local function close(t) + -- just a struct ? + end + + local function execute(t,query) + if query and query ~= "" then + local connection = t._connection_ + local result = mysql_execute_query(connection,query,#query) + if result == 0 then + return wrapresult(connection) else - return nt + -- mysql_error_number(connection) + return false, ffi_tostring(mysql_error_message(connection)) end end + return false end - return false -end -local mt = { __index = { - close = close, - execute = execute, + local mt = { + __index = { + close = close, + execute = execute, + } } -} -local function open(t,database,username,password,host,port) - local connection = mysql_open_connection( - t._session_, - host or "localhost", - username or "", - password or "", - database or "", - port or 0, - NULL, - 0 - ) - if connection ~= NULL then - local t = { - _connection_ = connection, - } - return setmetatable(t,mt) + -- session + + local function open(t,database,username,password,host,port) + local connection = mysql_open_connection( + t._session_, + host or "localhost", + username or "", + password or "", + database or "", + port or 0, + NULL, + 0 + ) + if connection ~= NULL then + if timeout then + execute(connection,formatters["SET SESSION connect_timeout=%s ;"](timeout)) + end + local t = { + _connection_ = connection, + } + return setmetatable(t,mt) + end end -end -local function message(t) - return mysql_error_message(t._session_) -end - -local function close(t) - -- dummy, as we have a global session -end + local function message(t) + return mysql_error_message(t._session_) + end -local mt = { - __index = { - connect = open, - close = close, - message = message, - } -} + local function close(t) + local connection = t._connection_ + if connection and connection ~= NULL then + ffi_gc(connection, mysql_close) + t.connection = nil + end + end -local function initialize() - local session = { - _session_ = mysql_initialize(instance) -- maybe share, single thread anyway + local mt = { + __index = { + connect = open, + close = close, + message = message, + }, } - return setmetatable(session,mt) -end --- -- -- -- + initializesession = function() + local session = { + _session_ = mysql_open_session(instance) -- maybe share, single thread anyway + } + return setmetatable(session,mt) + end -local function connect(session,specification) - return session:connect( - specification.database or "", - specification.username or "", - specification.password or "", - specification.host or "", - specification.port - ) end -local function error_in_connection(specification,action) - report_state("error in connection: [%s] user %s into %s at %s:%s", - action or "unknown", - specification.username or "no username", - specification.database or "no database", - specification.host or "no host", - specification.port or "no port" - ) -end +local executequery do -local function datafetched(specification,query,converter) - if not query or query == "" then - report_state("no valid query") - return { }, { } + local function connect(session,specification) + return session:connect( + specification.database or "", + specification.username or "", + specification.password or "", + specification.host or "", + specification.port + ) end - local id = specification.id - local session, connection - if id then - local c = cache[id] - if c then - session = c.session - connection = c.connection + + local function fetched(specification,query,converter) + if not query or query == "" then + report_state("no valid query") + return false end - if not connection then - session = initialize() - connection = connect(session,specification) + local id = specification.id + local session, connection + if id then + local c = cache[id] + if c then + session = c.session + connection = c.connection + end if not connection then - for i=1,nofretries do - sleep(retrydelay) - report_state("retrying to connect: [%s.%s] %s@%s to %s:%s", - id,i, - specification.database or "no database", - specification.username or "no username", - specification.host or "no host", - specification.port or "no port" - ) - connection = connect(session,specification) - if connection then - break - end + session = initializesession() + if not session then + return formatters["no session for %a"](id) end - end - if connection then - cache[id] = { session = session, connection = connection } - end - end - else - session = initialize() - connection = connect(session,specification) - if not connection then - for i=1,nofretries do - sleep(retrydelay) - report_state("retrying to connect: [%s] %s@%s to %s:%s", - i, - specification.database or "no database", - specification.username or "no username", - specification.host or "no host", - specification.port or "no port" - ) connection = connect(session,specification) - if connection then - break + if not connection then + return formatters["no connection for %a"](id) end + cache[id] = { session = session, connection = connection } + end + else + session = initializesession() + if not session then + return "no session" + end + connection = connect(session,specification) + if not connection then + return "no connection" end end - end - if not connection then - report_state("error in connection: %s@%s to %s:%s", + if not connection then + report_state("error in connection: %s@%s to %s:%s", specification.database or "no database", specification.username or "no username", specification.host or "no host", specification.port or "no port" ) - return { }, { } - end - query = lpegmatch(querysplitter,query) - local result, message, okay - for i=1,#query do - local q = query[i] - local r, m = connection:execute(q) - if m then - report_state("error in query, stage: %s",string.collapsespaces(q or "?")) - message = message and format("%s\n%s",message,m) or m + return "no connection" end - if type(r) == "table" then - result = r - okay = true - elseif not m then - okay = true + query = lpegmatch(querysplitter,query) + local result, okay + for i=1,#query do + local q = query[i] + local r, m = connection:execute(q) + if m then + report_state("error in query to host %a: %s",specification.host,string.collapsespaces(q or "?")) + if m then + report_state("message: %s",m) + end + end + local t = type(r) + if t == "table" then + result = r + okay = true + elseif t == "number" then + okay = true + end end - end - local data, keys - if result then - if converter then - data = converter.ffi(result) - else - keys = result.names - data = { } - for i=1,result.nofrows do - data[i] = result:hash() + if not okay then -- can go + -- why do we close a session + if connection then + connection:close() + end + if session then + session:close() end + if id then + cache[id] = nil + end + return "execution error" end - result:finish() -- result:close() - elseif message then - report_state("message %s",message) - end - if not keys then - keys = { } - end - if not data then - data = { } - end - if not id then - connection:close() - session:close() + local data, keys + if result then + if converter then + data = converter.ffi(result) + else + local _result_ = result._result_ + local noffields = result.noffields + local nofrows = result.nofrows + keys = result:getcolnames() + data = { } + if noffields > 0 and nofrows > 0 then + for i=1,nofrows do + local cells = { } + local row = mysql_fetch_row(_result_) + for j=1,noffields do + local s = row[j-1] + local k = keys[j] + if s == NULL then + cells[k] = "" + else + cells[k] = ffi_tostring(s) + end + end + data[i] = cells + end + end + end + result:close() + end + -- + if not id then + if connection then + connection:close() + end + if session then + session:close() + end + end + return false, data, keys end - return data, keys -end -local function execute(specification) - if trace_sql then - report_state("executing library") - end - if not validspecification(specification) then - report_state("error in specification") - return - end - local query = dataprepared(specification) - if not query then - report_state("error in preparation") - return - end - local data, keys = datafetched(specification,query,specification.converter) - if not data then - report_state("error in fetching") - return + local function datafetched(specification,query,converter) + local callokay, connectionerror, data, keys = pcall(fetched,specification,query,converter) + if not callokay then + report_state("call error, retrying") + callokay, connectionerror, data, keys = pcall(fetched,specification,query,converter) + elseif connectionerror then + report_state("error: %s, retrying",connectionerror) + callokay, connectionerror, data, keys = pcall(fetched,specification,query,converter) + end + if not callokay then + report_state("persistent call error") + elseif connectionerror then + report_state("persistent error: %s",connectionerror) + end + return data or { }, keys or { } end - local one = data[1] - if one then - setmetatable(data,{ __index = one } ) + + executequery = function(specification) + if trace_sql then + report_state("executing library") + end + if not validspecification(specification) then + report_state("error in specification") + return + end + local query = dataprepared(specification) + if not query then + report_state("error in preparation") + return + end + local data, keys = datafetched(specification,query,specification.converter) + if not data then + report_state("error in fetching") + return + end + local one = data[1] + if one then + setmetatable(data,{ __index = one } ) + end + return data, keys end - return data, keys + end local wraptemplate = [[ @@ -530,13 +581,14 @@ return function(result) if not result then return { } end - local nofrows = result.nofrows or 0 + local nofrows = result.nofrows if nofrows == 0 then return { } end - local noffields = result.noffields or 0 - local _result_ = result._result_ + local noffields = result.noffields local target = { } -- no %s needed here + local _result_ = result._result_ + -- we can share cells for i=1,nofrows do local cells = { } local row = mysql_fetch_row(_result_) @@ -552,7 +604,7 @@ return function(result) %s } end - result:finish() -- result:close() + result:close() return target end ]] @@ -560,9 +612,9 @@ end local celltemplate = "cells[%s]" methods.ffi = { - runner = function() end, -- never called - execute = execute, - initialize = initialize, -- returns session + runner = function() end, -- never called + execute = executequery, + initialize = initializesession, -- returns session usesfiles = false, wraptemplate = wraptemplate, celltemplate = celltemplate, -- cgit v1.2.3