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.lua371
1 files changed, 206 insertions, 165 deletions
diff --git a/tex/context/base/util-sql.lua b/tex/context/base/util-sql.lua
index 798ef44af..aeee7c09b 100644
--- a/tex/context/base/util-sql.lua
+++ b/tex/context/base/util-sql.lua
@@ -6,30 +6,44 @@ if not modules then modules = { } end modules ['util-sql'] = {
license = "see context related readme files"
}
--- Of course we could use a library but we don't want another depedency and
--- there is a bit of flux in these libraries. Also, we want the data back in
--- a way that we like.
-
--- Todo: buffer templates when files.
+-- Of course we could use a library but we don't want another depedency and there is
+-- a bit of flux in these libraries. Also, we want the data back in a way that we
+-- like.
+--
+-- This is the first of set of sql related modules that are providing functionality
+-- for a web based framework that we use for typesetting (related) services. We're
+-- talking of session management, job ticket processing, storage, (xml) file processing
+-- and dealing with data from databases (often ambitiously called database publishing).
+--
+-- There is no generic solution for such services, but from our perspective, as we use
+-- 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.
local format = string.format
+local random = math.random
local rawset, setmetatable, loadstring, type = rawset, setmetatable, loadstring, type
local P, S, V, C, Cs, Ct, Cc, Cg, Cf, patterns, lpegmatch = lpeg.P, lpeg.S, lpeg.V, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cf, lpeg.patterns, lpeg.match
local concat = table.concat
-local osclock = os.clock or os.time
-local fastserialize = table.fastserialize
-local lpegmatch = lpeg.match
+local osuuid = os.uuid
+local osclock = os.clock or os.time
-local trace_sql = false trackers.register("sql.trace",function(v) trace_sql = v end)
-local report_state = logs.reporter("sql")
+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)
+local report_state = logs.reporter("sql")
-utilities.sql = utilities.sql or { }
-local sql = utilities.sql
+utilities.sql = utilities.sql or { }
+local sql = utilities.sql
local replacetemplate = utilities.templates.replace
local loadtemplate = utilities.templates.load
+local methods = { }
+sql.methods = methods
+
+sql.serialize = table.fastserialize
+sql.deserialize = table.deserialize
+
local defaults = { __index =
{
resultfile = "result.dat",
@@ -44,22 +58,21 @@ local defaults = { __index =
},
}
-local engine = "mysql"
-
-local runners = { -- --defaults-extra-file="%inifile"
- mysql = [[mysql --user="%username%" --password="%password%" --host="%host%" --port=%port% --database="%database%" < "%queryfile%" > "%resultfile%"]],
-}
-
-sql.runners = runners
-
-- 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.
local separator = P("\t")
local newline = patterns.newline
-local entry = C((1-separator-newline)^0) -- C 10% faster than Cs
local empty = Cc("")
+local entry = C((1-separator-newline)^0) -- C 10% faster than Cs
+
+local unescaped = P("\\n") / "\n"
+ + P("\\t") / "\t"
+ + P("\\\\") / "\\"
+
+local entry = Cs((unescaped + (1-separator-newline))^0) -- C 10% faster than Cs but Cs needed due to nesting
+
local getfirst = Ct( entry * (separator * (entry+empty))^0) + newline
local skipfirst = (1-newline)^1 * newline
local getfirstline = C((1-newline)^0)
@@ -157,16 +170,39 @@ local function validspecification(specification)
return true
end
-local function dataprepared(specification)
- local query = false
- if specification.template then
- query = replacetemplate(specification.template,specification.variables)
- elseif specification.templatefile then
- query = loadtemplate(specification.templatefile,specification.variables)
+local function preparetemplate(specification)
+ local template = specification.template
+ if template then
+ local query = replacetemplate(template,specification.variables,'sql')
+ if not query then
+ report_state("error in template: %s",template)
+ elseif trace_queries then
+ report_state("query from template: %s",query)
+ end
+ return query
+ end
+ local templatefile = specification.templatefile
+ if templatefile then
+ local query = loadtemplate(templatefile,specification.variables,'sql')
+ if not query then
+ report_state("error in template file %q",templatefile)
+ elseif trace_queries then
+ report_state("query from template file %q: %s",templatefile,query)
+ end
+ return query
end
+ report_state("no query template or templatefile")
+end
+
+
+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
@@ -175,17 +211,17 @@ local function dataprepared(specification)
end
end
-local function datafetched(specification)
- local command = replacetemplate(runners[engine],specification)
+local function datafetched(specification,runner)
+ local command = replacetemplate(runner,specification)
if trace_sql then
local t = osclock()
report_state("command: %s",command)
- os.execute(command)
+ local okay = os.execute(command)
report_state("fetchtime: %.3f sec",osclock()-t) -- not okay under linux
+ return okay == 0
else
- os.execute(command)
+ return os.execute(command) == 0
end
- return true
end
local function dataloaded(specification)
@@ -215,66 +251,100 @@ end
sql.splitdata = splitdata
-local methods = { }
-sql.methods = methods
-
-- todo: new, etc
-local function fetch(specification)
+local function execute(specification)
if trace_sql then
- report_state("fetching")
+ report_state("executing")
end
if not validspecification(specification) then
- report("error in specification")
+ report_state("error in specification")
return
end
if not dataprepared(specification) then
- report("error in preparation")
+ report_state("error in preparation")
return
end
- if not datafetched(specification) then
- report("error in fetching")
+ 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)
--- local data = datafetched(specification)
if not data then
- report("error in loading")
+ report_state("error in loading")
return
end
local data, keys = dataconverted(data)
if not data then
- report("error in converting")
+ report_state("error in converting")
return
end
return data, keys
end
--- local function reuse(specification)
--- if trace_sql then
--- report_state("reusing")
--- end
--- if not validspecification(specification) then
--- report("error in specification")
--- return
--- end
--- local data = dataloaded(specification)
--- if not data then
--- report("error in loading")
--- return
--- end
--- local data, keys = dataconverted(data)
--- if not data then
--- report("error in converting")
--- return
--- end
--- return data, keys
--- end
-
-sql.fetch = fetch
-
methods.client = {
- fetch = fetch,
+ runner = [[mysql --batch --user="%username%" --password="%password%" --host="%host%" --port=%port% --database="%database%" < "%queryfile%" > "%resultfile%"]],
+ execute = execute,
+ serialize = serialize,
+ deserialize = deserialize,
+}
+
+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")
+ 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
+ return data, keys
+end
+
+methods.lmxsql = {
+ runner = [[lmx-sql %host% %port% "%username%" "%password%" "%database%" "%queryfile%" "%resultfile%"]],
+ execute = execute,
+ serialize = serialize,
+ deserialize = deserialize,
}
local mysql = nil
@@ -294,17 +364,7 @@ local function validspecification(specification)
return true
end
-local function dataprepared(specification)
- local query = false
- if specification.template then
- query = replacetemplate(specification.template,specification.variables)
- elseif specification.templatefile then
- query = loadtemplate(specification.templatefile,specification.variables)
- end
- if query then
- return query
- end
-end
+local dataprepared = preparetemplate
local function connect(session,specification)
return session:connect(
@@ -317,9 +377,17 @@ local function connect(session,specification)
end
local whitespace = patterns.whitespace^0
-local quoted = patterns.quoted
local separator = P(";")
-local query = whitespace * Cs((quoted + 1 - separator)^1 * Cc(";")) * whitespace
+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 query = whitespace
+ * Cs((quoted + 1 - separator)^1 * Cc(";"))
+ * whitespace
local splitter = Ct(query * (separator * query)^0)
local function datafetched(specification,query)
@@ -348,7 +416,9 @@ local function datafetched(specification,query)
for i=1,#query do
local q = query[i]
result, message = connection:execute(q)
--- io.savedata("e:/tmp/oeps.sql",q)
+ if message then
+ report_state("error in query: %s",string.collapsespaces(q))
+ end
end
if not result and id then
if session then
@@ -361,7 +431,11 @@ local function datafetched(specification,query)
connection = connect(session,specification)
cache[id] = { session = session, connection = connection }
for i=1,#query do
- result, message = connection:execute(query[i])
+ local q = query[i]
+ result, message = connection:execute(q)
+ if message then
+ report_state("error in query: %s",string.collapsespaces(q))
+ end
end
end
local data, keys
@@ -397,7 +471,7 @@ local function datafetched(specification,query)
return data, keys
end
-local function fetch(specification)
+local function execute(specification)
if not mysql then
local lib = require("luasql.mysql")
if lib then
@@ -407,31 +481,49 @@ local function fetch(specification)
end
end
if trace_sql then
- report_state("fetching")
+ report_state("executing")
end
if not validspecification(specification) then
- report("error in specification")
+ report_state("error in specification")
return
end
local query = dataprepared(specification)
if not query then
- report("error in preparation")
+ report_state("error in preparation")
return
end
local data, keys = datafetched(specification,query)
if not data then
- report("error in fetching")
+ report_state("error in fetching")
return
end
return data, keys
end
methods.library = {
- fetch = fetch,
+ runner = function() end, -- never called
+ execute = execute,
+ serialize = serialize,
+ deserialize = deserialize,
}
-- -- --
+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
+end
+
+sql.setmethod("client")
+
-- local data = utilities.sql.prepare {
-- templatefile = "test.sql",
-- variables = { },
@@ -466,93 +558,42 @@ methods.library = {
-- presets = "...",
-- }
--- -- --
-
-local e_pattern = lpeg.replacer { { '\\"','\\\\""' }, {'"','""'}, {'\\\n', "\\n" }, {'\\\r', "\\r" }, {'\t', " " } }
-local u_pattern = lpeg.replacer { { '\\\\','\\' } }
-local u_pattern = lpeg.replacer { { '\\\\','\\' }, { "\n","\\n" } }
-
--- library:
-
-function methods.library.serialize(t)
- local str = fastserialize(t,"return")
- local escaped = lpegmatch(e_pattern,str)
- return escaped
-end
-
-function methods.library.deserialize(str)
- local unescaped = lpegmatch(u_pattern,str)
- if not unescaped then
- return
- end
- local code = loadstring(unescaped)
- if not code then
- return
- end
- code = code()
- if not code then
- return
- end
- return code
-end
-
--- client
-
-local e_pattern = lpeg.replacer { { '\\"','\\\\""' }, {'"','""'}, {'\\\n', "\\n" }, {'\\\r', "\\r" } }
-local u_pattern = lpeg.replacer { { '\\\\','\\' } }
-
-function methods.client.serialize(t)
- return lpegmatch(e_pattern,fastserialize(t,"return"))
-end
-
-function methods.client.deserialize(str)
- local unescaped = lpegmatch(u_pattern,str)
- if not unescaped then
- return
- end
- local code = loadstring(unescaped)
- if not code then
- return
- end
- code = code()
- if not code then
- return
- end
- return code
-end
-
-sql.serialize = methods.client.serialize
-sql.deserialize = methods.client.deserialize
-
-function sql.escape(str)
- return lpegmatch(e_pattern,str)
-end
-
-function sql.unescape(str)
- return lpegmatch(u_pattern,str)
-end
-
--- local s = sql.serialize { a = 1, b = { 4, { 5, 6 } }, c = { d = 7, e = 'f"g\nh' } }
--- local u = sql.unescape(s)
--- local t = sql.deserialize(s)
--- inspect(s)
--- inspect(u)
--- inspect(t)
+sql.tokens = {
+ length = 42,
+ new = function()
+ return format("%s+%05x",osuuid(),random(1,0xFFFFF)) -- 36 + 1 + 5 = 42
+ end,
+}
-- -- --
if tex and tex.systemmodes then
- function sql.prepare(specification)
+ local droptable = table.drop
+ local threshold = 16 * 1024 -- use slower but less memory hungry variant
+
+ function sql.prepare(specification,tag)
+ -- could go into tuc if needed
+ -- todo: serialize per column
+ local filename = format("%s-sql-result-%s.tuc",tex.jobname,tag or "last")
if tex.systemmodes["first"] then
- return sql.fetch(specification)
+ local data, keys = sql.execute(specification)
+ if not data then
+ data = { }
+ end
+ if not keys then
+ keys = { }
+ end
+ io.savedata(filename,droptable({ data = data, keys = keys },#keys*#data>threshold))
+ return data, keys
else
- return sql.reuse(specification)
+ local result = table.load(filename)
+ return result.data, result.keys
end
end
else
- sql.prepare = sql.fetch
+ sql.prepare = sql.execute
end