summaryrefslogtreecommitdiff
path: root/tex/context/base/util-sql.lua
diff options
context:
space:
mode:
Diffstat (limited to 'tex/context/base/util-sql.lua')
-rw-r--r--tex/context/base/util-sql.lua721
1 files changed, 138 insertions, 583 deletions
diff --git a/tex/context/base/util-sql.lua b/tex/context/base/util-sql.lua
index 4e9a5bcd4..e2edf0ffe 100644
--- a/tex/context/base/util-sql.lua
+++ b/tex/context/base/util-sql.lua
@@ -19,62 +19,42 @@ if not modules then modules = { } end modules ['util-sql'] = {
-- context in a regular tds tree (the standard distribution) it makes sense to put shared
-- code in the context distribution. That way we don't need to reinvent wheels every time.
--- For some reason the sql lib partially fails in luatex when creating hashed row. So far
--- we couldn't figure it out (some issue with adapting the table that is passes as first
--- argument in the fetch routine. Apart from this it looks like the mysql binding has some
--- efficiency issues (like creating a keys and types table for each row) but that could be
--- optimized. Anyhow, fecthing results can be done as follows:
-
-- We use the template mechanism from util-tpl which inturn is just using the dos cq
-- windows convention of %whatever% variables that I've used for ages.
--- local function collect_1(r)
--- local t = { }
--- for i=1,r:numrows() do
--- t[#t+1] = r:fetch({},"a")
--- end
--- return t
--- end
---
--- local function collect_2(r)
--- local keys = r:getcolnames()
--- local n = #keys
--- local t = { }
--- for i=1,r:numrows() do
--- local v = { r:fetch() }
--- local r = { }
--- for i=1,n do
--- r[keys[i]] = v[i]
--- end
--- t[#t+1] = r
--- end
--- return t
--- end
+-- util-sql-imp-client.lua
+-- util-sql-imp-library.lua
+-- util-sql-imp-swiglib.lua
+-- util-sql-imp-lmxsql.lua
+
+-- local sql = require("util-sql")
--
--- local function collect_3(r)
--- local keys = r:getcolnames()
--- local n = #keys
--- local t = { }
--- for i=1,r:numrows() do
--- local v = r:fetch({},"n")
--- local r = { }
--- for i=1,n do
--- r[keys[i]] = v[i]
--- end
--- t[#t+1] = r
--- end
--- return t
--- end
+-- local converter = sql.makeconverter {
+-- { name = "id", type = "number" },
+-- { name = "data",type = "string" },
+-- }
--
--- On a large table with some 8 columns (mixed text and numbers) we get the following
--- timings (the 'a' alternative is already using the more efficient variant in the
--- binding).
+-- local execute = sql.methods.swiglib.execute
+-- -- local execute = sql.methods.library.execute
+-- -- local execute = sql.methods.client.execute
+-- -- local execute = sql.methods.lmxsql.execute
--
--- collect_1 : 1.31
--- collect_2 : 1.39
--- collect_3 : 1.75
+-- result = execute {
+-- presets = {
+-- host = "localhost",
+-- username = "root",
+-- password = "test",
+-- database = "test",
+-- id = "test", -- forces persistent session
+-- },
+-- template = "select * from `test` where `id` > %criterium% ;",
+-- variables = {
+-- criterium = 2,
+-- },
+-- converter = converter
+-- }
--
--- Some, as a workaround for this 'bug' the second alternative can be used.
+-- inspect(result)
local format, match = string.format, string.match
local random = math.random
@@ -102,12 +82,18 @@ local loadtemplate = utilities.templates.load
local methods = { }
sql.methods = methods
+local helpers = { }
+sql.helpers = helpers
+
local serialize = table.fastserialize
local deserialize = table.deserialize
sql.serialize = serialize
sql.deserialize = deserialize
+helpers.serialize = serialize -- bonus
+helpers.deserialize = deserialize -- bonus
+
local defaults = { __index =
{
resultfile = "result.dat",
@@ -122,92 +108,105 @@ local defaults = { __index =
},
}
--- Experiments with an p/action demonstrated that there is not much gain. We could do a runtime
--- capture but creating all the small tables is not faster and it doesn't work well anyway.
+table.setmetatableindex(sql.methods,function(t,k)
+ require("util-sql-imp-"..k)
+ return rawget(t,k)
+end)
-local separator = P("\t")
-local newline = patterns.newline
-local empty = Cc("")
+-- converters
-local entry = C((1-separator-newline)^0) -- C 10% faster than Cs
-
-local unescaped = P("\\n") / "\n"
- + P("\\t") / "\t"
- + P("\\\\") / "\\"
+local converters = { }
+sql.converters = converters
-local entry = Cs((unescaped + (1-separator-newline))^0) -- C 10% faster than Cs but Cs needed due to nesting
+local function makeconverter(entries,celltemplate,wraptemplate)
+ local shortcuts = { }
+ local assignments = { }
+ local function cell(i)
+ return format(celltemplate,i,i)
+ end
+ for i=1,#entries do
+ local entry = entries[i]
+ local nam = entry.name
+ local typ = entry.type
+ if typ == "boolean" then
+ assignments[i] = format("[%q] = booleanstring(%s),",nam,cell(i))
+ elseif typ == "number" then
+ assignments[i] = format("[%q] = tonumber(%s),",nam,cell(i))
+ elseif type(typ) == "function" then
+ local c = #converters + 1
+ converters[c] = typ
+ shortcuts[#shortcuts+1] = format("local fun_%s = converters[%s]",c,c)
+ assignments[i] = format("[%q] = fun_%s(%s),",nam,c,cell(i))
+ elseif type(typ) == "table" then
+ local c = #converters + 1
+ converters[c] = typ
+ shortcuts[#shortcuts+1] = format("local tab_%s = converters[%s]",c,c)
+ assignments[i] = format("[%q] = tab_%s[%s],",nam,#converters,cell(i))
+ elseif typ == "deserialize" then
+ assignments[i] = format("[%q] = deserialize(%s),",nam,cell(i))
+ else
+ assignments[i] = format("[%q] = %s,",nam,cell(i))
+ end
+ end
+ local code = format(wraptemplate,concat(shortcuts,"\n"),concat(assignments,"\n "))
+ local func = loadstring(code)
+ return func and func()
+end
-local getfirst = Ct( entry * (separator * (entry+empty))^0) + newline
-local skipfirst = (1-newline)^1 * newline
-local getfirstline = C((1-newline)^0)
+function sql.makeconverter(entries)
+ local fields = { }
+ for i=1,#entries do
+ fields[i] = format("`%s`",entries[i].name)
+ end
+ fields = concat(fields, ", ")
+ local converter = {
+ fields = fields
+ }
+ table.setmetatableindex(converter, function(t,k)
+ local sqlmethod = methods[k]
+ local v = makeconverter(entries,sqlmethod.celltemplate,sqlmethod.wraptemplate)
+ t[k] = v
+ return v
+ end)
+ return converter, fields
+end
-local cache = { }
+-- helper for libraries:
-local function splitdata(data) -- todo: hash on first line
- if data == "" then
- if trace_sql then
- report_state("no data")
- end
- return { }, { }
- end
- local first = lpegmatch(getfirstline,data)
- if not first then
- if trace_sql then
- report_state("no data")
- end
- return { }, { }
+local function validspecification(specification)
+ local presets = specification.presets
+ if type(presets) == "string" then
+ presets = dofile(presets)
end
- local p = cache[first]
- if p then
- -- report_state("reusing: %s",first)
- local entries = lpegmatch(p.parser,data)
- return entries or { }, p.keys
- elseif p == false then
- return { }, { }
- elseif p == nil then
- local keys = lpegmatch(getfirst,first) or { }
- if #keys == 0 then
- if trace_sql then
- report_state("no banner")
- end
- cache[first] = false
- return { }, { }
- end
- -- quite generic, could be a helper
- local n = #keys
- if n == 0 then
- report_state("no fields")
- cache[first] = false
- return { }, { }
- end
- if n == 1 then
- local key = keys[1]
- if trace_sql then
- report_state("one field with name",key)
- end
- p = Cg(Cc(key) * entry)
- else
- for i=1,n do
- local key = keys[i]
- if trace_sql then
- report_state("field %s has name %q",i,key)
- end
- local s = Cg(Cc(key) * entry)
- if p then
- p = p * separator * s
- else
- p = s
- end
- end
- end
- p = Cf(Ct("") * p,rawset) * newline^1
- p = skipfirst * Ct(p^0)
- cache[first] = { parser = p, keys = keys }
- local entries = lpegmatch(p,data)
- return entries or { }, keys
+ if type(presets) == "table" then
+ setmetatable(presets,defaults)
+ setmetatable(specification,{ __index = presets })
+ else
+ setmetatable(specification,defaults)
end
+ return true
end
+helpers.validspecification = validspecification
+
+local whitespace = patterns.whitespace^0
+local eol = patterns.eol
+local separator = P(";")
+local escaped = patterns.escaped
+local dquote = patterns.dquote
+local squote = patterns.squote
+local dsquote = squote * squote
+---- quoted = patterns.quoted
+local quoted = dquote * (escaped + (1-dquote))^0 * dquote
+ + squote * (escaped + dsquote + (1-squote))^0 * squote
+local comment = P("--") * (1-eol) / ""
+local query = whitespace
+ * Cs((quoted + comment + 1 - separator)^1 * Cc(";"))
+ * whitespace
+local splitter = Ct(query * (separator * query)^0)
+
+helpers.querysplitter = splitter
+
-- I will add a bit more checking.
local function validspecification(specification)
@@ -263,411 +262,35 @@ local function preparetemplate(specification)
report_state("no query template or templatefile")
end
+helpers.preparetemplate = preparetemplate
-local function dataprepared(specification)
- local query = preparetemplate(specification)
- if query then
- io.savedata(specification.queryfile,query)
- os.remove(specification.resultfile)
- if trace_queries then
- report_state("query: %s",query)
- end
- return true
- else
- -- maybe push an error
- os.remove(specification.queryfile)
- os.remove(specification.resultfile)
- end
-end
-
-local function datafetched(specification,runner)
- local command = replacetemplate(runner,specification)
- if trace_sql then
- local t = osclock()
- report_state("command: %s",command)
- local okay = os.execute(command)
- report_state("fetchtime: %.3f sec",osclock()-t) -- not okay under linux
- return okay == 0
- else
- return os.execute(command) == 0
- end
-end
-
-local function dataloaded(specification)
- if trace_sql then
- local t = osclock()
- local data = io.loaddata(specification.resultfile) or ""
- report_state("datasize: %.3f MB",#data/1024/1024)
- report_state("loadtime: %.3f sec",osclock()-t)
- return data
- else
- return io.loaddata(specification.resultfile) or ""
- end
-end
-
-local function dataconverted(data)
- if trace_sql then
- local t = osclock()
- local data, keys = splitdata(data)
- report_state("converttime: %.3f",osclock()-t)
- report_state("keys: %s ",#keys)
- report_state("entries: %s ",#data)
- return data, keys
- else
- return splitdata(data)
- end
-end
-
-sql.splitdata = splitdata
-
--- todo: new, etc
-
-local function execute(specification)
- if trace_sql then
- report_state("executing client")
- end
- if not validspecification(specification) then
- report_state("error in specification")
- return
- end
- if not dataprepared(specification) then
- report_state("error in preparation")
- return
- end
- if not datafetched(specification,methods.client.runner) then
- report_state("error in fetching, query: %s",string.collapsespaces(io.loaddata(specification.queryfile)))
- return
- end
- local data = dataloaded(specification)
- if not data then
- report_state("error in loading")
- return
- end
- local data, keys = dataconverted(data)
- if not data then
- report_state("error in converting")
- return
- end
- local one = data[1]
- if one then
- setmetatable(data,{ __index = one } )
- end
- return data, keys
-end
-
-methods.client = {
- runner = [[mysql --batch --user="%username%" --password="%password%" --host="%host%" --port=%port% --database="%database%" < "%queryfile%" > "%resultfile%"]],
- execute = execute,
- serialize = serialize,
- deserialize = deserialize,
- usesfiles = true,
-}
-
-local function dataloaded(specification)
- if trace_sql then
- local t = osclock()
- local data = table.load(specification.resultfile)
- report_state("loadtime: %.3f sec",osclock()-t)
- return data
- else
- return table.load(specification.resultfile)
- end
-end
-
-local function dataconverted(data)
- if trace_sql then
- local data, keys = data.data, data.keys
- report_state("keys: %s ",#keys)
- report_state("entries: %s ",#data)
- return data, keys
- else
- return data.data, data.keys
- end
-end
-
-local function execute(specification)
- if trace_sql then
- report_state("executing lmxsql")
- end
- if not validspecification(specification) then
- report_state("error in specification")
- return
- end
- if not dataprepared(specification) then
- report_state("error in preparation")
- return
- end
- if not datafetched(specification,methods.lmxsql.runner) then
- report_state("error in fetching, query: %s",string.collapsespaces(io.loaddata(specification.queryfile)))
- return
- end
- local data = dataloaded(specification)
- if not data then
- report_state("error in loading")
- return
- end
- local data, keys = dataconverted(data)
- if not data then
- report_state("error in converting")
- return
- end
- local one = data[1]
- if one then
- setmetatable(data,{ __index = one } )
- end
- return data, keys
-end
-
-methods.lmxsql = {
- runner = [[lmx-sql %host% %port% "%username%" "%password%" "%database%" "%queryfile%" "%resultfile%"]],
- execute = execute,
- serialize = serialize,
- deserialize = deserialize,
- usesfiles = true,
-}
-
-local mysql = nil
-local cache = { }
-
-local function validspecification(specification)
- local presets = specification.presets
- if type(presets) == "string" then
- presets = dofile(presets)
- end
- if type(presets) == "table" then
- setmetatable(presets,defaults)
- setmetatable(specification,{ __index = presets })
- else
- setmetatable(specification,defaults)
- end
- return true
-end
-
-local dataprepared = preparetemplate
-
-local function connect(session,specification)
- return session:connect(
- specification.database or "",
- specification.username or "",
- specification.password or "",
- specification.host or "",
- specification.port
- )
-end
-
-local whitespace = patterns.whitespace^0
-local eol = patterns.eol
-local separator = P(";")
-local escaped = patterns.escaped
-local dquote = patterns.dquote
-local squote = patterns.squote
-local dsquote = squote * squote
----- quoted = patterns.quoted
-local quoted = dquote * (escaped + (1-dquote))^0 * dquote
- + squote * (escaped + dsquote + (1-squote))^0 * squote
-local comment = P("--") * (1-eol) / ""
-local query = whitespace
- * Cs((quoted + comment + 1 - separator)^1 * Cc(";"))
- * whitespace
-local splitter = Ct(query * (separator * query)^0)
+-- -- -- we delay setting this -- -- --
-local function datafetched(specification,query,converter)
- if not query or query == "" then
- report_state("no valid query")
- return { }, { }
- end
- 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
- session = mysql()
- connection = connect(session,specification)
- cache[id] = { session = session, connection = connection }
- end
- else
- session = mysql()
- connection = connect(session,specification)
- end
- 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(splitter,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 1: %s",string.collapsespaces(q))
- message = message and format("%s\n%s",message,m) or m
- end
- local t = type(r)
- if t == "userdata" then
- result = r
- okay = true
- elseif t == "number" then
- okay = true
- end
- end
- if not okay and id then
- if session then
- session:close()
- end
- if connection then
- connection:close()
- end
- session = mysql() -- maybe not needed
- connection = connect(session,specification)
- cache[id] = { session = session, connection = connection }
- for i=1,#query do
- local q = query[i]
- local r, m = connection:execute(q)
- if m then
- report_state("error in query, stage 2: %s",string.collapsespaces(q))
- message = message and format("%s\n%s",message,m) or m
- end
- local t = type(r)
- if t == "userdata" 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(result,deserialize)
- else
- keys = result:getcolnames()
- if keys then
- local n = result:numrows() or 0
- if n == 0 then
- data = { }
- -- elseif n == 1 then
- -- -- data = { result:fetch({},"a") }
- else
- data = { }
- -- for i=1,n do
- -- data[i] = result:fetch({},"a")
- -- end
- local k = #keys
- for i=1,n do
- local v = { result:fetch() }
- local d = { }
- for i=1,k do
- d[keys[i]] = v[i]
- end
- data[#data+1] = d
- end
- end
- end
- end
- 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()
- end
- return data, keys
-end
+local currentmethod
-local function execute(specification)
- if not mysql then
- local lib = require("luasql.mysql")
- if lib then
- mysql = lib.mysql
- else
- report_state("error in loading luasql.mysql")
- end
- end
- 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
+local function firstexecute(...)
+ local execute = methods[currentmethod].execute
+ sql.execute = execute
+ return execute(...)
end
-methods.library = {
- runner = function() end, -- never called
- execute = execute,
- serialize = serialize,
- deserialize = deserialize,
- usesfiles = false,
-}
-
--- -- --
-
-local currentmethod
-
function sql.setmethod(method)
- local m = methods[method]
- if m then
- currentmethod = method
- sql.execute = m.execute
- return m
- else
- return methods[currentmethod]
- end
+ currentmethod = method
+ sql.execute = firstexecute
end
sql.setmethod("client")
-- helper:
-local execute = sql.execute
-
function sql.usedatabase(presets,datatable)
local name = datatable or presets.datatable
if name then
- local method = presets.method and sql.methods[presets.method] or sql.methods.client
- local base = presets.database or "test"
- local basename = format("`%s`.`%s`",base,name)
- m_execute = execute
- deserialize = deserialize
- serialize = serialize
- if method then
- m_execute = method.execute or m_execute
- deserialize = method.deserialize or deserialize
- serialize = method.serialize or serialize
- end
- local execute
+ local method = presets.method and sql.methods[presets.method] or sql.methods.client
+ local base = presets.database or "test"
+ local basename = format("`%s`.`%s`",base,name)
+ local execute = nil
+ local m_execute = method.execute
if method.usesfiles then
local queryfile = presets.queryfile or format("%s-temp.sql",name)
local resultfile = presets.resultfile or format("%s-temp.dat",name)
@@ -756,74 +379,6 @@ sql.tokens = {
-- -- --
-local converters = { }
-
-sql.converters = converters
-
-local template = [[
-local converters = utilities.sql.converters
-
-local tostring = tostring
-local tonumber = tonumber
-local toboolean = toboolean
-
-%s
-
-return function(result,deserialize)
- if not result then
- return { }
- end
- local nofrows = result:numrows() or 0
- if nofrows == 0 then
- return { }
- end
- local data = { }
- for i=1,nofrows do
- local v = { result:fetch() }
- data[i] = {
- %s
- }
- end
- return data
-end
-]]
-
-function sql.makeconverter(entries,deserialize)
- local shortcuts = { }
- local assignments = { }
- local fields = { }
- for i=1,#entries do
- local entry = entries[i]
- local nam = entry.name
- local typ = entry.type
- if typ == "boolean" then
- assignments[i] = format("[%q] = toboolean(v[%s],true),",nam,i)
- elseif typ == "number" then
- assignments[i] = format("[%q] = tonumber(v[%s]),",nam,i)
- elseif type(typ) == "function" then
- local c = #converters + 1
- converters[c] = typ
- shortcuts[#shortcuts+1] = format("local fun_%s = converters[%s]",c,c)
- assignments[i] = format("[%q] = fun_%s(v[%s]),",nam,c,i)
- elseif type(typ) == "table" then
- local c = #converters + 1
- converters[c] = typ
- shortcuts[#shortcuts+1] = format("local tab_%s = converters[%s]",c,c)
- assignments[i] = format("[%q] = tab_%s[v[%s]],",nam,#converters,i)
- elseif typ == "deserialize" then
- assignments[i] = format("[%q] = deserialize(v[%s]),",nam,i)
- else
- assignments[i] = format("[%q] = v[%s],",nam,i)
- end
- fields[#fields+1] = format("`%s`",nam)
- end
- local code = string.format(template,table.concat(shortcuts,"\n"),table.concat(assignments,"\n "))
- local func = loadstring(code)
- if type(func) == "function" then
- return func(), concat(fields, ", ")
- end
-end
-
-- local func, code = sql.makeconverter {
-- { name = "a", type = "number" },
-- { name = "b", type = "string" },