--[[ 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 = {} --[[ Runs an sql query and receives the 3 arguments back, prints a nice error message on fail, and returns true on success. ]] 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 --[[ Continuously tries to perform an sql statement until it goes through ]] 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 --[[ 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. ]] 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 --[[ Binds an argument to as statement with nice error reporting on failure 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 error(string.format("Failed call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2) end end local oldconfigure = configure db.conn = db.sqlassert(sql.open(config.db)) function configure(...) --Create sql tables assert(db.conn:exec(queries.create_table_authors)) --Create a fake "anonymous" user, so we don't run into trouble --so that no one runs into trouble being able to paste under this account. assert(db.conn:exec(queries.insert_anon_author)) --If/when an author deletes their account, all posts --and comments by that author are also deleted (on --delete cascade) this is intentional. This also --means that all comments by other users on a post --an author makes will also be deleted. -- --Post text uses zlib compression assert(db.conn:exec(queries.create_table_posts)) --Store the raw text so people can download it later, maybe --we can use it for "download as image" or "download as pdf" --in the future too. Stil stored zlib compressed assert(db.conn:exec(queries.create_table_raw_text)) --Maybe we want to store images one day? assert(db.conn:exec(queries.create_table_images)) --Comments on a post assert(db.conn:exec(queries.create_table_comments)) --Tags for a post assert(db.conn:exec(queries.create_table_tags)) --Index for tags assert(db.conn:exec(queries.create_index_tags)) --Store a cookie for logged in users. Logged in users can edit --their own posts, and edit their biographies. assert(db.conn:exec(queries.create_table_session)) return oldconfigure(...) end configure() function db.close() db.conn:close() end return db