smr/src/lua/db.lua

241 lines
5.5 KiB
Lua

--[[ md
@name lua/db
## Overview
Does most of the database interaction.
Creates default empty database during configure()
Notably, holds a connection to the open sqlite3 database in .conn
]]
--[[ sh
@name sql/table
echo "digraph schema {" \
"$(cat doc/schema/*.dot)" \
"}" | dot -Tsvg
]]
local sql = require("lsqlite3")
local queries = require("queries")
local config = require("config")
local db = {}
--[[ md
@name lua/db
### 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
### 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
### 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) -- prints 'Hello, world!'
end
]]
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
--[[ md
@name lua/db
### db.sqlbind
Binds an argument to a prepared statement,
with nice error reporting on failure.
Wraps {{lsqlite/stmnt/bind_name}}
with better error reporting.
Parameters:
0. stmnt - {{lsqlite/stmnt}} - The prepared statement from {{sqlite/db/prepare}}
0. call - {{lua/string}} - Literal string, options are `bind` for most types,
or `bind_blob` for strings that may contain embedded nulls
0. position - {{lua/number}} - The argument position to bind to,
does not support named parameters
0. data - Any - the data to bind
Returns nothing
]]
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
--[[ md
@name lua/db
### db.close()
Closes the database connection. Not called during normal operation, used to
assist in unit testing.
No parameters
No returns
]]
function db.close()
db.conn:close()
end
return db