smr/src/lua/db.lua

200 lines
4.9 KiB
Lua

--[[ md
@name lua/db
Does most of the database interaction.
Notably, holds a connection to the open sqlite3 database in .conn
]]
local sql = require("lsqlite3")
local queries = require("queries")
local config = require("config")
local db = {}
--[[ md
@name lua/db/sqlassert
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
Parameters:
0. r - {{lsqlite/stmnt}} | {{lua/nil}} - The userdata returned from
{{lsqlite/db/prepare}}
0. errcode - {{lua/nil}} | {{lua/number}} - If the first argument back from
{{lsqlite/db/prepare}} is nil, this second argument is a numeric errorcode,
see {{lsqlite/errcodes}}
0. err - {{lua/nil}} | {{lua/string}} - The string error returned from
{{lsqlite/db/prepare}}. Only non-nil if the first return value was nil. A
string message describing what went wrong in the statment. If this argument is
also {{lua/nil}}, this function retrives the error mssage from
{{lsqlite/db/errmsg}}.
Returns:
0. r - {{lua/userdata}} | {{lua/nil}} - The first argument passed in. Used so
that error checking and assignment can all be done on a single line.
Example:
> db = require("db")
> query = db.sqlassert(db.conn:parepare("SELECT 'Hello, world!'"))
]]
function db.sqlassert(r, errcode, err)
if not r then
if err then
error(string.format("%d: %s",errcode, err))
elseif errcode then
error(string.format("%d: %s",errcode, db.conn:errmsg()))
end
end
return r
end
--[[ md
@name lua/db/do_sql
Continuously tries to perform an sql statement until it goes through. This function may call {{lua/coroutine/yield}}
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned form {{lsqlite/db/prepare}}
Returns:
0. err - {{lua/number}} - The error code returned from running the statement. Will be `lsqlite.OK` on success, see {{lsqlite/errcodes}}
Example:
> sql = require("lsqlite3")
> configure = function(...) end -- Mock smr environment
> db = require("db")
> configure()
> query = db.conn:prepare("SELECT 'Hello, world!';")
> assert(db.do_sql(query))
]]
function db.do_sql(stmnt)
if not stmnt then error("No statement",2) end
local err
local i = 0
repeat
err = stmnt:step()
if err == sql.BUSY then
i = i + 1
coroutine.yield()
end
until(err ~= sql.BUSY or i > 10)
assert(i < 10, "Database busy")
return err
end
--[[ md
@name lua/db/sql_rows
Provides an iterator that loops over results in an sql statement or throws an
error, then resets the statement after the loop is done.
Returned iterator returns varargs, so the values can be unpacked in-line in the
for loop. This statement is approximately the same as {{sqlite/stmt/rows}}, but
may yield when the db connection is busy, and continue execution when the
connection is free again.
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The userdata returned from {{sqlite/db/prepare}}
Returns:
0. iterator - {{lua/iterator}} - The iterator function that returns varargs of the returns from the sql statement.
Example:
> db = require("db")
> query = db.conn:prepare("SELECT 'Hello, world!';")
> for row in db.sql_rows(query) do
> print(row)
> end
Hello, world!
]]
function db.sql_rows(stmnt)
if not stmnt then error("No statement",2) end
local err
return function()
err = stmnt:step()
if err == sql.BUSY then
coroutine.yield()
elseif err == sql.ROW then
return unpack(stmnt:get_values())
elseif err == sql.DONE then
stmnt:reset()
return nil
else
stmnt:reset()
local msg = string.format(
"SQL Iteration failed: %s : %s\n%s",
tostring(err),
db.conn:errmsg(),
debug.traceback()
)
log(LOG_CRIT,msg)
error(msg)
end
end
end
--[[
@name lua/db/sqlbind
Binds an argument to as statement with nice error reporting on failure. Approximatly the same as {{lsqlite/stmt/bind_names}}, but with better error reporting.
stmnt :: sql.stmnt - the prepared sql statemnet
call :: string - a string "bind" or "bind_blob"
position :: number - the argument position to bind to
data :: string - The data to bind
]]
function db.sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
local f = stmnt[call](stmnt,position,data)
if f ~= sql.OK then
local errs = string.format(
"Failed call %s(%d,%q): %s",
call,
position,
data,
db.conn:errmsg()
)
log(LOG_ERR,errs)
error(errs,2)
end
end
local oldconfigure = configure
db.conn = db.sqlassert(sql.open(config.db))
function configure(...)
local statements = {
"create_table_authors",
"insert_anon_author",
"create_table_posts",
"create_table_raw_text",
"create_table_images",
"create_table_comments",
"create_table_tags",
"create_index_tags",
"create_table_session"
}
-- ipairs() needed, "create table authors" must be executed before
-- "insert anon author"
for _, statement in ipairs(statements) do
db.sqlassert(db.conn:exec(queries[statement]))
end
return oldconfigure(...)
end
function db.close()
db.conn:close()
end
return db